From ac9d29b971f9d1d214d2917e2685aa3e869c675a Mon Sep 17 00:00:00 2001 From: Jocelyn Fiat Date: Tue, 5 Sep 2017 15:54:40 +0200 Subject: [PATCH] Added basic webapi system to ROC CMS system. Added sql_delete routine to replace sql_modify with "DELETE FROM .." sql statement. Fixed filter setup when a module has more than one filter. Fixed filter setup for site,admin and webapi modes. Added CMS_AUTH_FILTER, and check if user is already authenticated, then skip following auth filters. Added specific webapi handler classes for root, user, access token, ... Added user profile system to the core module. Moved /user/{uid} from auth module to core module. Added possibility to add html before and after a cms form. (useful to add a form before or after, as nested form are forbidden). Now theme can be installed using roc install command. --- cms-safe.ecf | 11 +- cms.ecf | 1 + examples/demo/roc.cfg | 4 + examples/demo/site/config/cms.ini | 6 +- library/model/src/user/cms_user.e | 36 -- library/model/src/user/cms_user_profile.e | 72 ++++ .../store/cms_storage_store_sql.e | 6 + .../sqlite3/src/cms_storage_sqlite3.e | 6 + modules/auth/cms_auth_strategy_filter.e | 29 ++ modules/auth/cms_authentication_module.e | 56 ++- .../auth/cms_authentication_module_webapi.e | 47 +++ modules/basic_auth/cms_basic_auth_module.e | 9 + .../basic_auth/cms_basic_auth_module_webapi.e | 36 ++ .../basic_auth/filter/cms_basic_auth_filter.e | 2 +- .../handler/cms_basic_auth_login_handler.e | 4 - modules/core/site/scripts/user_profile.sql | 6 + .../feed_aggregator/feed_aggregator_module.e | 2 +- modules/oauth20/filter/cms_oauth_20_filter.e | 2 +- modules/openid/filter/cms_openid_filter.e | 2 +- .../filter/cms_session_auth_filter.e | 2 +- .../cms_session_auth_storage_sql.e | 2 - .../site/scripts/user_profile.sql | 6 + src/configuration/cms_setup.e | 33 ++ src/hooks/cms_hook_auto_register.e | 6 +- src/hooks/cms_hook_core_manager.e | 24 +- src/hooks/cms_hook_webapi_response_alter.e | 30 ++ src/kernel/form/cms_form.e | 59 ++- src/modules/{ => core}/cms_core_module.e | 65 ++- .../user => modules/core}/cms_user_api.e | 194 ++++++--- src/modules/core/cms_user_profile_api.e | 87 ++++ .../core/handler/user}/cms_user_handler.e | 35 +- .../handler/user}/cms_user_view_response.e | 52 ++- .../persistence/core/cms_core_storage_i.e | 0 .../persistence/core/cms_core_storage_sql_i.e | 0 .../persistence/user/cms_user_storage_i.e | 0 .../persistence/user/cms_user_storage_null.e | 0 .../persistence/user/cms_user_storage_sql_i.e | 2 +- .../user_profile/cms_user_profile_storage_i.e | 66 +++ .../cms_user_profile_storage_null.e | 42 ++ .../cms_user_profile_storage_sql.e | 212 ++++++++++ .../cms_access_token_webapi_auth_filter.e | 39 ++ .../webapi/cms_access_token_webapi_handler.e | 182 +++++++++ .../webapi/cms_basic_webapi_auth_filter.e | 43 ++ .../core/webapi/cms_core_module_webapi.e | 57 +++ .../core/webapi/cms_root_webapi_handler.e | 36 ++ .../core/webapi/cms_user_webapi_handler.e | 80 ++++ src/modules/{ => debug}/cms_debug_module.e | 0 src/persistence/sql/cms_proxy_storage_sql.e | 7 +- src/persistence/sql/cms_storage_sql_i.e | 7 +- src/service/cms_administrable.e | 25 +- src/service/cms_api.e | 134 ++++++- src/service/cms_execution.e | 164 +++++++- src/service/cms_module.e | 2 +- src/service/cms_module_administration.e | 10 + src/service/cms_with_module_administration.e | 36 ++ .../service/filter/cms_auth_filter.e | 23 +- src/service/response/cms_response.e | 340 +--------------- src/service/response/cms_response_i.e | 376 ++++++++++++++++++ .../error/bad_request_error_cms_response.e | 3 +- .../error/forbidden_error_cms_response.e | 1 - .../internal_server_error_cms_response.e | 3 +- .../error/not_found_error_cms_response.e | 3 +- .../not_implemented_error_cms_response.e | 3 +- .../response/generic_view_cms_response.e | 2 +- src/service/response/hm_webapi_response.e | 51 +++ src/service/response/home_cms_response.e | 4 +- src/service/response/json_webapi_response.e | 201 ++++++++++ src/service/response/md_webapi_response.e | 70 ++++ src/service/response/webapi_response.e | 44 ++ src/service/webapi/cms_module_webapi.e | 90 +++++ src/service/webapi/cms_webapi_auth_filter.e | 15 + src/service/webapi/cms_webapi_handler.e | 100 +++++ src/service/webapi/cms_with_webapi.e | 36 ++ themes/admin/assets/css/style.css | 107 +++++ themes/admin/assets/favicon.ico | Bin 0 -> 994 bytes themes/admin/assets/js/jquery-1.10.2.min.js | 6 + themes/admin/assets/js/popup_search.js | 8 + themes/admin/assets/scss/style.scss | 110 +++++ themes/admin/debug.tpl | 38 ++ themes/admin/page.tpl | 101 +++++ themes/admin/theme.info | 14 + themes/min/assets/css/style.css | 0 themes/min/assets/favicon.ico | Bin 0 -> 994 bytes themes/min/assets/scss/style.scss | 0 themes/min/page.tpl | 66 +++ themes/min/theme.info | 15 + tools/roc/roc_install_command.e | 160 +++++++- tools/roc/roc_install_copy_parameters.e | 39 ++ 88 files changed, 3552 insertions(+), 553 deletions(-) create mode 100644 library/model/src/user/cms_user_profile.e create mode 100644 modules/auth/cms_auth_strategy_filter.e create mode 100644 modules/auth/cms_authentication_module_webapi.e create mode 100644 modules/basic_auth/cms_basic_auth_module_webapi.e create mode 100644 modules/core/site/scripts/user_profile.sql create mode 100644 modules/user_profile/site/scripts/user_profile.sql create mode 100644 src/hooks/cms_hook_webapi_response_alter.e rename src/modules/{ => core}/cms_core_module.e (65%) rename src/{service/user => modules/core}/cms_user_api.e (68%) create mode 100644 src/modules/core/cms_user_profile_api.e rename {modules/auth => src/modules/core/handler/user}/cms_user_handler.e (67%) rename {modules/auth => src/modules/core/handler/user}/cms_user_view_response.e (55%) rename src/{ => modules/core}/persistence/core/cms_core_storage_i.e (100%) rename src/{ => modules/core}/persistence/core/cms_core_storage_sql_i.e (100%) rename src/{ => modules/core}/persistence/user/cms_user_storage_i.e (100%) rename src/{ => modules/core}/persistence/user/cms_user_storage_null.e (100%) rename src/{ => modules/core}/persistence/user/cms_user_storage_sql_i.e (99%) create mode 100644 src/modules/core/persistence/user_profile/cms_user_profile_storage_i.e create mode 100644 src/modules/core/persistence/user_profile/cms_user_profile_storage_null.e create mode 100644 src/modules/core/persistence/user_profile/cms_user_profile_storage_sql.e create mode 100644 src/modules/core/webapi/cms_access_token_webapi_auth_filter.e create mode 100644 src/modules/core/webapi/cms_access_token_webapi_handler.e create mode 100644 src/modules/core/webapi/cms_basic_webapi_auth_filter.e create mode 100644 src/modules/core/webapi/cms_core_module_webapi.e create mode 100644 src/modules/core/webapi/cms_root_webapi_handler.e create mode 100644 src/modules/core/webapi/cms_user_webapi_handler.e rename src/modules/{ => debug}/cms_debug_module.e (100%) create mode 100644 src/service/cms_with_module_administration.e rename modules/auth/cms_auth_filter_i.e => src/service/filter/cms_auth_filter.e (57%) create mode 100644 src/service/response/cms_response_i.e create mode 100644 src/service/response/hm_webapi_response.e create mode 100644 src/service/response/json_webapi_response.e create mode 100644 src/service/response/md_webapi_response.e create mode 100644 src/service/response/webapi_response.e create mode 100644 src/service/webapi/cms_module_webapi.e create mode 100644 src/service/webapi/cms_webapi_auth_filter.e create mode 100644 src/service/webapi/cms_webapi_handler.e create mode 100644 src/service/webapi/cms_with_webapi.e create mode 100644 themes/admin/assets/css/style.css create mode 100644 themes/admin/assets/favicon.ico create mode 100644 themes/admin/assets/js/jquery-1.10.2.min.js create mode 100644 themes/admin/assets/js/popup_search.js create mode 100644 themes/admin/assets/scss/style.scss create mode 100644 themes/admin/debug.tpl create mode 100644 themes/admin/page.tpl create mode 100644 themes/admin/theme.info create mode 100644 themes/min/assets/css/style.css create mode 100644 themes/min/assets/favicon.ico create mode 100644 themes/min/assets/scss/style.scss create mode 100644 themes/min/page.tpl create mode 100644 themes/min/theme.info create mode 100644 tools/roc/roc_install_copy_parameters.e diff --git a/cms-safe.ecf b/cms-safe.ecf index c91a8c7..9d82c88 100644 --- a/cms-safe.ecf +++ b/cms-safe.ecf @@ -1,5 +1,5 @@ - + @@ -7,9 +7,12 @@ /CVS$ /EIFGENs$ - - + + + + @@ -20,9 +23,11 @@ + + diff --git a/cms.ecf b/cms.ecf index 446990a..80e88e4 100644 --- a/cms.ecf +++ b/cms.ecf @@ -20,6 +20,7 @@ + diff --git a/examples/demo/roc.cfg b/examples/demo/roc.cfg index 9f2d2b8..d18c27f 100644 --- a/examples/demo/roc.cfg +++ b/examples/demo/roc.cfg @@ -3,7 +3,11 @@ "project": "demo-safe.ecf", "location": ".", "site_directory": "site", + "themes": { + "admin": { "location": "../../themes/admin", "mode": "link" } + }, "modules": { + "demo": { "location": "modules/demo" }, "core": { "location": "../../modules/core" }, "admin": { "location": "../../modules/admin" }, "auth": { "location": "../../modules/auth" }, diff --git a/examples/demo/site/config/cms.ini b/examples/demo/site/config/cms.ini index 0414897..78179f2 100644 --- a/examples/demo/site/config/cms.ini +++ b/examples/demo/site/config/cms.ini @@ -57,13 +57,15 @@ output=site\db\mails #openid.token= #oauth.token= +[webapi] +mode=on [administration] base_path=/roc-admin -#theme=admin +theme=admin # CMS Installation, are accessible by "all", "none" or uppon "permission". (default is none) installation_access=all [dev] # masquerade: all, none, permission. Default is none. -masquerade=none +masquerade=all diff --git a/library/model/src/user/cms_user.e b/library/model/src/user/cms_user.e index 8f29c6c..8fd6faf 100644 --- a/library/model/src/user/cms_user.e +++ b/library/model/src/user/cms_user.e @@ -98,19 +98,6 @@ feature -- Roles roles: detachable LIST [CMS_USER_ROLE] -- If set, list of roles for current user. -feature -- Access: data - - item (k: READABLE_STRING_GENERAL): detachable ANY assign put - -- Additional item data associated with key `k'. - do - if attached items as tb then - Result := tb.item (k) - end - end - - items: detachable STRING_TABLE [detachable ANY] - -- Additional data. - feature -- Status report has_id: BOOLEAN @@ -223,29 +210,6 @@ feature -- Element change: roles roles := lst end -feature -- Change element: data - - put (d: like item; k: READABLE_STRING_GENERAL) - -- Associate data item `d' with key `k'. - local - tb: like items - do - tb := items - if tb = Void then - create tb.make (1) - items := tb - end - tb.force (d, k) - end - - remove (k: READABLE_STRING_GENERAL) - -- Remove data item associated with key `k'. - do - if attached items as tb then - tb.remove (k) - end - end - feature -- Status change mark_not_active diff --git a/library/model/src/user/cms_user_profile.e b/library/model/src/user/cms_user_profile.e new file mode 100644 index 0000000..55a1c8f --- /dev/null +++ b/library/model/src/user/cms_user_profile.e @@ -0,0 +1,72 @@ +note + description: "[ + User profile used to extend information associated with a {CMS_USER}. + ]" + date: "$Date$" + revision: "$Revision$" + +class + CMS_USER_PROFILE + +inherit + TABLE_ITERABLE [READABLE_STRING_32, READABLE_STRING_GENERAL] + +create + make + +feature {NONE} -- Initialization + + make + -- Create Current profile. + do + create items.make (0) + end + +feature -- Access + + item alias "[]" (k: READABLE_STRING_GENERAL): detachable READABLE_STRING_32 + -- Profile item associated with key `k`. + do + Result := items.item (k) + end + + has_key (k: READABLE_STRING_GENERAL): BOOLEAN + -- Has a profile item associated with key `k`? + do + Result := items.has (k) + end + + count: INTEGER + do + Result := items.count + end + + is_empty: BOOLEAN + do + Result := items.is_empty + end + +feature -- Change + + force (v: READABLE_STRING_GENERAL; k: READABLE_STRING_GENERAL) + -- Associated value `v` with key `k`. + do + items.force (v.to_string_32, k) + end + +feature -- Access + + new_cursor: TABLE_ITERATION_CURSOR [READABLE_STRING_32, READABLE_STRING_GENERAL] + -- Fresh cursor associated with current structure + do + Result := items.new_cursor + end + +feature {NONE} -- Implementation + + items: STRING_TABLE [READABLE_STRING_32] + +;note + copyright: "2011-2014, Javier Velilla, Jocelyn Fiat, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/library/persistence/implementation/store/cms_storage_store_sql.e b/library/persistence/implementation/store/cms_storage_store_sql.e index ed364df..a403cf3 100644 --- a/library/persistence/implementation/store/cms_storage_store_sql.e +++ b/library/persistence/implementation/store/cms_storage_store_sql.e @@ -110,6 +110,12 @@ feature -- Query sql_post_execution end + sql_delete (a_sql_statement: STRING; a_params: detachable STRING_TABLE [detachable ANY]) + -- + do + sql_modify (a_sql_statement, a_params) + end + sql_rows_count: INTEGER -- Number of rows for last sql execution. do diff --git a/library/persistence/sqlite3/src/cms_storage_sqlite3.e b/library/persistence/sqlite3/src/cms_storage_sqlite3.e index 1ab1f6e..2ddbc53 100644 --- a/library/persistence/sqlite3/src/cms_storage_sqlite3.e +++ b/library/persistence/sqlite3/src/cms_storage_sqlite3.e @@ -230,6 +230,12 @@ feature -- Operation end end + sql_delete (a_sql_statement: STRING; a_params: detachable STRING_TABLE [detachable ANY]) + -- + do + sql_modify (a_sql_statement, a_params) + end + sqlite_arguments (a_params: STRING_TABLE [detachable ANY]): ARRAYED_LIST [SQLITE_BIND_ARG [ANY]] local k: READABLE_STRING_GENERAL diff --git a/modules/auth/cms_auth_strategy_filter.e b/modules/auth/cms_auth_strategy_filter.e new file mode 100644 index 0000000..6b81869 --- /dev/null +++ b/modules/auth/cms_auth_strategy_filter.e @@ -0,0 +1,29 @@ +note + description: "Summary description for {CMS_AUTH_FILTER_WITH_LOGOUT}." + author: "" + date: "$Date$" + revision: "$Revision$" + +deferred class + CMS_AUTH_STRATEGY_FILTER + +inherit + CMS_AUTH_FILTER + redefine + set_current_user + end + +feature -- Basic operations + + auth_strategy: STRING + deferred + end + + set_current_user (u: CMS_USER) + do + Precursor (u) + -- Record auth strategy: + api.set_execution_variable ({CMS_AUTHENTICATION_MODULE}.auth_strategy_execution_variable_name, auth_strategy) + end + +end diff --git a/modules/auth/cms_authentication_module.e b/modules/auth/cms_authentication_module.e index 9447581..e8ff039 100644 --- a/modules/auth/cms_authentication_module.e +++ b/modules/auth/cms_authentication_module.e @@ -124,8 +124,6 @@ feature -- Router a_router.handle ("/account/new-password", create {WSF_URI_AGENT_HANDLER}.make (agent handle_new_password(a_api, ?, ?)), a_router.methods_get_post) a_router.handle ("/account/reset-password", create {WSF_URI_AGENT_HANDLER}.make (agent handle_reset_password(a_api, ?, ?)), a_router.methods_get_post) a_router.handle ("/account/change/{field}", create {WSF_URI_TEMPLATE_AGENT_HANDLER}.make (agent handle_change_field (a_api, ?, ?)), a_router.methods_get_post) - - a_router.handle ("/user/{uid}", create {CMS_USER_HANDLER}.make (a_api), a_router.methods_get) end feature -- Hooks configuration @@ -206,6 +204,23 @@ feature -- Hooks configuration end end +feature -- Handler / Constants + + auth_strategy_execution_variable_name: STRING = "auth_strategy" + -- Exevc + + auth_strategy (req: WSF_REQUEST): detachable READABLE_STRING_8 + -- Strategy used by current authentication. + -- note: if user is authenticated.. + do + if + attached {READABLE_STRING_GENERAL} req.execution_variable (auth_strategy_execution_variable_name) as s and then + s.is_valid_as_string_8 + then + Result := s.to_string_8 + end + end + feature -- Handler handle_account (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE) @@ -214,10 +229,13 @@ feature -- Handler l_user: detachable CMS_USER b: STRING lnk: CMS_LOCAL_LINK + f: CMS_FORM + tf: WSF_FORM_TEXT_INPUT do create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api) create b.make_empty l_user := r.user + create f.make (r.location, "roccms-user-view") if attached smarty_template_block (Current, "account_info", api) as l_tpl_block then l_tpl_block.set_weight (-10) r.add_block (l_tpl_block, "content") @@ -225,6 +243,30 @@ feature -- Handler debug ("cms") r.add_warning_message ("Error with block [resources_page]") end + if l_user /= Void then + create tf.make_with_text ("username", l_user.name) + tf.set_label ("Username") + f.extend (tf) + if attached l_user.email as l_email then + create tf.make_with_text ("email", l_email.to_string_32) + tf.set_label ("Email") + f.extend (tf) + end + if attached l_user.profile_name as l_prof_name then + create tf.make_with_text ("profile_name", l_prof_name) + tf.set_label ("Profile name") + f.extend (tf) + end + create tf.make_with_text ("creation", api.formatted_date_time_yyyy_mm_dd (l_user.creation_date)) + tf.set_label ("Creation date") + f.extend (tf) + + if attached l_user.last_login_date as dt then + create tf.make_with_text ("last_login", api.formatted_date_time_ago (dt)) + tf.set_label ("Last login") + f.extend (tf) + end + end end if r.is_authenticated then @@ -237,6 +279,9 @@ feature -- Handler r.add_to_primary_tabs (lnk) end + api.hooks.invoke_form_alter (f, Void, r) + f.append_to_html (r.wsf_theme, b) + r.set_main_content (b) if l_user = Void then @@ -251,10 +296,12 @@ feature -- Handler l_user: detachable CMS_USER b: STRING lnk: CMS_LOCAL_LINK + l_form: CMS_FORM do create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api) create b.make_empty l_user := r.user + create l_form.make (r.location, "roccms-user-edit") if attached smarty_template_block (Current, "account_edit", api) as l_tpl_block then l_tpl_block.set_weight (-10) r.add_block (l_tpl_block, "content") @@ -262,6 +309,7 @@ feature -- Handler debug ("cms") r.add_warning_message ("Error with block [resources_page]") end + -- Build CMS form... end create lnk.make ("View", "account/") lnk.set_weight (1) @@ -287,6 +335,8 @@ feature -- Handler f.append_to_html (r.wsf_theme, b) end + l_form.append_to_html (r.wsf_theme, b) + r.set_main_content (b) if l_user = Void then @@ -336,7 +386,7 @@ feature -- Handler loc: STRING do create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api) - if attached {READABLE_STRING_8} api.execution_variable ("auth_strategy") as l_auth_strategy then + if attached auth_strategy (req) as l_auth_strategy then loc := l_auth_strategy else loc := "" diff --git a/modules/auth/cms_authentication_module_webapi.e b/modules/auth/cms_authentication_module_webapi.e new file mode 100644 index 0000000..6ea639e --- /dev/null +++ b/modules/auth/cms_authentication_module_webapi.e @@ -0,0 +1,47 @@ +note + description: "Summary description for {CMS_AUTHENTICATION_MODULE_WEBAPI}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + CMS_AUTHENTICATION_MODULE_WEBAPI + +inherit + CMS_MODULE_WEBAPI [CMS_AUTHENTICATION_MODULE] + redefine + permissions + end + +create + make + +feature -- Security + + permissions: LIST [READABLE_STRING_8] + -- List of permission ids, used by this module, and declared. + do + Result := Precursor + Result.force ("admin users") + end + +feature {NONE} -- Router/administration + + setup_webapi_router (a_router: WSF_ROUTER; a_api: CMS_API) + -- + do + a_router.handle ("/info", create {WSF_URI_AGENT_HANDLER}.make (agent handle_info (?, ?, a_api)), a_router.methods_get) + end + +feature -- Request handling + + handle_info (req: WSF_REQUEST; res: WSF_RESPONSE; api: CMS_API) + local + m: WSF_HTML_PAGE_RESPONSE + do + create m.make + m.set_body ("{%"info%":%"" + api.setup.site_name + "%"}") + res.send (m) + end + +end diff --git a/modules/basic_auth/cms_basic_auth_module.e b/modules/basic_auth/cms_basic_auth_module.e index e0af8fa..3493a54 100644 --- a/modules/basic_auth/cms_basic_auth_module.e +++ b/modules/basic_auth/cms_basic_auth_module.e @@ -19,6 +19,8 @@ inherit setup_hooks end + CMS_WITH_WEBAPI + CMS_HOOK_BLOCK create @@ -33,6 +35,13 @@ feature {NONE} -- Initialization description := "Service to manage basic authentication" end +feature {CMS_EXECUTION} -- Administration + + webapi: CMS_BASIC_AUTH_MODULE_WEBAPI + do + create Result.make (Current) + end + feature -- Access name: STRING = "basic_auth" diff --git a/modules/basic_auth/cms_basic_auth_module_webapi.e b/modules/basic_auth/cms_basic_auth_module_webapi.e new file mode 100644 index 0000000..e5cc766 --- /dev/null +++ b/modules/basic_auth/cms_basic_auth_module_webapi.e @@ -0,0 +1,36 @@ +note + description: "Summary description for {CMS_BASIC_AUTH_MODULE_WEBAPI}." + date: "$Date$" + revision: "$Revision$" + +class + CMS_BASIC_AUTH_MODULE_WEBAPI + +inherit + CMS_MODULE_WEBAPI [CMS_BASIC_AUTH_MODULE] + redefine + filters + end + +create + make + +feature {NONE} -- Router/administration + + setup_webapi_router (a_router: WSF_ROUTER; a_api: CMS_API) + -- + do + end + +feature -- Access: filter + + filters (a_api: CMS_API): detachable LIST [WSF_FILTER] + -- Possibly list of Filter's module. + do + create {ARRAYED_LIST [WSF_FILTER]} Result.make (1) + Result.extend (create {CMS_BASIC_AUTH_FILTER}.make (a_api)) + end +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/modules/basic_auth/filter/cms_basic_auth_filter.e b/modules/basic_auth/filter/cms_basic_auth_filter.e index ed46978..1baf777 100644 --- a/modules/basic_auth/filter/cms_basic_auth_filter.e +++ b/modules/basic_auth/filter/cms_basic_auth_filter.e @@ -9,7 +9,7 @@ class CMS_BASIC_AUTH_FILTER inherit - CMS_AUTH_FILTER_I + CMS_AUTH_STRATEGY_FILTER REFACTORING_HELPER diff --git a/modules/basic_auth/handler/cms_basic_auth_login_handler.e b/modules/basic_auth/handler/cms_basic_auth_login_handler.e index d5130db..bebf989 100644 --- a/modules/basic_auth/handler/cms_basic_auth_login_handler.e +++ b/modules/basic_auth/handler/cms_basic_auth_login_handler.e @@ -15,9 +15,6 @@ inherit new_mapping as new_uri_mapping end - WSF_FILTER - - WSF_RESOURCE_HANDLER_HELPER redefine do_get @@ -34,7 +31,6 @@ feature -- execute -- Execute request handler. do execute_methods (req, res) - execute_next (req, res) end uri_execute (req: WSF_REQUEST; res: WSF_RESPONSE) diff --git a/modules/core/site/scripts/user_profile.sql b/modules/core/site/scripts/user_profile.sql new file mode 100644 index 0000000..c9adf65 --- /dev/null +++ b/modules/core/site/scripts/user_profile.sql @@ -0,0 +1,6 @@ +CREATE TABLE user_profiles( + `uid` INTEGER NOT NULL CHECK("uid">=0), + `key` VARCHAR(255) NOT NULL, + `value` TEXT, + CONSTRAINT PK_uid_key PRIMARY KEY (uid,key) +); diff --git a/modules/feed_aggregator/feed_aggregator_module.e b/modules/feed_aggregator/feed_aggregator_module.e index 5ddeb55..0a851ad 100644 --- a/modules/feed_aggregator/feed_aggregator_module.e +++ b/modules/feed_aggregator/feed_aggregator_module.e @@ -57,7 +57,7 @@ feature {CMS_API} -- Module Initialization create feed_aggregator_api.make (api) end -feature {CMS_API} -- Access: API +feature {CMS_API, CMS_MODULE} -- Access: API feed_aggregator_api: detachable FEED_AGGREGATOR_API -- Eventual module api. diff --git a/modules/oauth20/filter/cms_oauth_20_filter.e b/modules/oauth20/filter/cms_oauth_20_filter.e index 420fe33..d1b9ee1 100644 --- a/modules/oauth20/filter/cms_oauth_20_filter.e +++ b/modules/oauth20/filter/cms_oauth_20_filter.e @@ -10,7 +10,7 @@ class CMS_OAUTH_20_FILTER inherit - CMS_AUTH_FILTER_I + CMS_AUTH_STRATEGY_FILTER rename make as make_filter end diff --git a/modules/openid/filter/cms_openid_filter.e b/modules/openid/filter/cms_openid_filter.e index bb44619..1840706 100644 --- a/modules/openid/filter/cms_openid_filter.e +++ b/modules/openid/filter/cms_openid_filter.e @@ -9,7 +9,7 @@ class CMS_OPENID_FILTER inherit - CMS_AUTH_FILTER_I + CMS_AUTH_STRATEGY_FILTER rename make as make_filter end diff --git a/modules/session_auth/filter/cms_session_auth_filter.e b/modules/session_auth/filter/cms_session_auth_filter.e index 2ddcb09..3977a76 100644 --- a/modules/session_auth/filter/cms_session_auth_filter.e +++ b/modules/session_auth/filter/cms_session_auth_filter.e @@ -9,7 +9,7 @@ class CMS_SESSION_AUTH_FILTER inherit - CMS_AUTH_FILTER_I + CMS_AUTH_STRATEGY_FILTER rename make as make_filter end diff --git a/modules/session_auth/persistence/cms_session_auth_storage_sql.e b/modules/session_auth/persistence/cms_session_auth_storage_sql.e index 466b0dd..23473e0 100644 --- a/modules/session_auth/persistence/cms_session_auth_storage_sql.e +++ b/modules/session_auth/persistence/cms_session_auth_storage_sql.e @@ -11,8 +11,6 @@ inherit CMS_PROXY_STORAGE_SQL - CMS_SESSION_AUTH_STORAGE_I - CMS_STORAGE_SQL_I REFACTORING_HELPER diff --git a/modules/user_profile/site/scripts/user_profile.sql b/modules/user_profile/site/scripts/user_profile.sql new file mode 100644 index 0000000..0461723 --- /dev/null +++ b/modules/user_profile/site/scripts/user_profile.sql @@ -0,0 +1,6 @@ +CREATE TABLE user_profiles( + `uid` INTEGER NOT NULL CHECK("uid">=0), + `key` VARCHAR(255) NOT NULL. + `value` TEXT. + CONSTRAINT PK_uid_key PRIMARY KEY (uid,key) +); diff --git a/src/configuration/cms_setup.e b/src/configuration/cms_setup.e index 48c78ee..47793d5 100644 --- a/src/configuration/cms_setup.e +++ b/src/configuration/cms_setup.e @@ -103,6 +103,24 @@ feature {NONE} -- Initialization site_theme_name := text_item_or_default ("site.theme", "default") set_theme (site_theme_name) + + -- Webapi + webapi_enabled := string_8_item_or_default ("webapi.mode", "off").is_case_insensitive_equal_general ("on") + + l_url := string_8_item ("webapi.base_path") + if l_url /= Void and then not l_url.is_empty then + if l_url [l_url.count] = '/' then + l_url := l_url.substring (1, l_url.count - 1) + end + if l_url [1] /= '/' then + l_url := "/" + l_url + end + create webapi_base_path.make_from_string (l_url) + else + create webapi_base_path.make_from_string (default_webapi_base_path) + end + + -- Administration l_url := string_8_item ("administration.base_path") if l_url /= Void and then not l_url.is_empty then @@ -117,6 +135,7 @@ feature {NONE} -- Initialization create administration_base_path.make_from_string (default_administration_base_path) end administration_theme_name := text_item_or_default ("administration.theme", theme_name) -- TODO: Default to builtin theme? + end feature -- Access @@ -314,11 +333,19 @@ feature -- Access: Site -- Optional path defining the front page. -- By default "" or "/". + webapi_enabled: BOOLEAN + -- Is WebAPI enabled? + + webapi_base_path: IMMUTABLE_STRING_8 + -- Web API base url, default=`default_webapi_base_path`. + administration_base_path: IMMUTABLE_STRING_8 -- Administration base url, default=`default_administration_base_path`. feature {NONE} -- Constants + default_webapi_base_path: STRING = "/api" + default_administration_base_path: STRING = "/admin" feature -- Settings @@ -338,6 +365,12 @@ feature -- Settings end end + set_webapi_mode + -- Switch to webapi mode. + do + set_site_mode + end + set_administration_mode -- Switch to administration mode. --| - Change theme diff --git a/src/hooks/cms_hook_auto_register.e b/src/hooks/cms_hook_auto_register.e index bc86223..a53c9ee 100644 --- a/src/hooks/cms_hook_auto_register.e +++ b/src/hooks/cms_hook_auto_register.e @@ -36,9 +36,13 @@ feature -- Hook if attached {CMS_HOOK_RESPONSE_ALTER} Current as h_resp then a_hooks.subscribe_to_response_alter_hook (h_resp) end + if attached {CMS_HOOK_WEBAPI_RESPONSE_ALTER} Current as h_resp then + a_hooks.subscribe_to_webapi_response_alter_hook (h_resp) + end + end note - copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/src/hooks/cms_hook_core_manager.e b/src/hooks/cms_hook_core_manager.e index b504d46..30d4ce8 100644 --- a/src/hooks/cms_hook_core_manager.e +++ b/src/hooks/cms_hook_core_manager.e @@ -60,6 +60,28 @@ feature -- Hook: response end end +feature -- Hook: webapi response + + subscribe_to_webapi_response_alter_hook (h: CMS_HOOK_WEBAPI_RESPONSE_ALTER) + -- Add `h' as subscriber of response alter hooks CMS_HOOK_WEBAPI_RESPONSE_ALTER. + do + subscribe_to_hook (h, {CMS_HOOK_WEBAPI_RESPONSE_ALTER}) + end + + invoke_webapi_response_alter (a_response: WEBAPI_RESPONSE) + -- Invoke response alter hook for response `a_response'. + do + if attached subscribers ({CMS_HOOK_WEBAPI_RESPONSE_ALTER}) as lst then + across + lst as ic + loop + if attached {CMS_HOOK_WEBAPI_RESPONSE_ALTER} ic.item as h then + h.webapi_response_alter (a_response) + end + end + end + end + feature -- Hook: menu_system_alter subscribe_to_menu_system_alter_hook (h: CMS_HOOK_MENU_SYSTEM_ALTER) @@ -110,7 +132,7 @@ feature -- Hook: form_alter -- Add `h' as subscriber of form alter hooks CMS_HOOK_FORM_ALTER, -- and response `a_response'. do - subscribe_to_hook (h, {CMS_HOOK_MENU_ALTER}) + subscribe_to_hook (h, {CMS_HOOK_FORM_ALTER}) end invoke_form_alter (a_form: CMS_FORM; a_form_data: detachable WSF_FORM_DATA; a_response: CMS_RESPONSE) diff --git a/src/hooks/cms_hook_webapi_response_alter.e b/src/hooks/cms_hook_webapi_response_alter.e new file mode 100644 index 0000000..35d31a4 --- /dev/null +++ b/src/hooks/cms_hook_webapi_response_alter.e @@ -0,0 +1,30 @@ +note + description: "[ + Hook providing a way to alter a webapi response. + ]" + date: "$Date$" + revision: "$Revision$" + +deferred class + CMS_HOOK_WEBAPI_RESPONSE_ALTER + +inherit + CMS_HOOK + +feature -- Hook + + webapi_response_alter (a_response: WEBAPI_RESPONSE) + deferred + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" + source: "[ + Eiffel Software + 5949 Hollister Ave., Goleta, CA 93117 USA + Telephone 805-685-1006, Fax 805-685-6869 + Website http://www.eiffel.com + Customer support http://support.eiffel.com + ]" +end diff --git a/src/kernel/form/cms_form.e b/src/kernel/form/cms_form.e index 3cb690b..2d75f81 100644 --- a/src/kernel/form/cms_form.e +++ b/src/kernel/form/cms_form.e @@ -10,11 +10,68 @@ inherit WSF_FORM rename process as process_form + redefine + append_to_html end create make +feature -- Access + + before_widgets: detachable ARRAYED_LIST [WSF_WIDGET] + -- Optional widget before the Current form. + + after_widgets: detachable ARRAYED_LIST [WSF_WIDGET] + -- Optional widget after the Current form. + +feature -- Element change + + put_widget_before_form (w: WSF_WIDGET) + local + lst: like before_widgets + do + lst := before_widgets + if lst = Void then + create lst.make (1) + before_widgets := lst + end + lst.extend (w) + end + + put_widget_after_form (w: WSF_WIDGET) + local + lst: like after_widgets + do + lst := after_widgets + if lst = Void then + create lst.make (1) + after_widgets := lst + end + lst.extend (w) + end + +feature -- Conversion + + append_to_html (a_theme: WSF_THEME; a_html: STRING_8) + do + if attached before_widgets as lst then + across + lst as ic + loop + ic.item.append_to_html (a_theme, a_html) + end + end + Precursor (a_theme, a_html) + if attached after_widgets as lst then + across + lst as ic + loop + ic.item.append_to_html (a_theme, a_html) + end + end + end + feature -- Basic operation prepare (a_response: CMS_RESPONSE) @@ -40,6 +97,6 @@ feature -- Basic operation end note - copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/src/modules/cms_core_module.e b/src/modules/core/cms_core_module.e similarity index 65% rename from src/modules/cms_core_module.e rename to src/modules/core/cms_core_module.e index bd65886..b5a0524 100644 --- a/src/modules/cms_core_module.e +++ b/src/modules/core/cms_core_module.e @@ -10,10 +10,17 @@ inherit CMS_MODULE redefine initialize, + setup_hooks, install, permissions end + CMS_WITH_WEBAPI + + CMS_HOOK_AUTO_REGISTER + + CMS_HOOK_FORM_ALTER + create make @@ -41,10 +48,16 @@ feature {CMS_API} -- Module Initialization feature {CMS_API} -- Module management install (a_api: CMS_API) + local + l_parent_loc: PATH do -- Schema if attached a_api.storage.as_sql_storage as l_sql_storage then - l_sql_storage.sql_execute_file_script (a_api.module_resource_location (Current, (create {PATH}.make_from_string ("scripts")).extended (name + ".sql")), Void) + l_parent_loc := a_api.module_resource_location (Current, create {PATH}.make_from_string ("scripts")) + l_sql_storage.sql_execute_file_script (l_parent_loc.extended (name + ".sql"), Void) + if not l_sql_storage.has_error then + l_sql_storage.sql_execute_file_script (l_parent_loc.extended ("user_profile.sql"), Void) + end if l_sql_storage.has_error then a_api.logger.put_error ("Could not initialize database for module [" + name + "]", generating_type) @@ -81,6 +94,7 @@ feature {CMS_API} -- Module management --! external configuration file? --! at the moment we only have 1 admin to the whole site. --! is that ok? + l_anonymous_role.add_permission ("view any page") a_api.user_api.save_user_role (l_anonymous_role) @@ -101,6 +115,7 @@ feature -- Router setup_router (a_router: WSF_ROUTER; a_api: CMS_API) -- do + a_router.handle ("/user/{uid}", create {CMS_USER_HANDLER}.make (a_api), a_router.methods_get) end feature -- Security @@ -117,6 +132,54 @@ feature -- Security Result.force ("edit path_alias") end +feature {CMS_EXECUTION} -- Administration + + webapi: CMS_CORE_MODULE_WEBAPI + do + create Result.make (Current) + end + +feature -- Hooks + + setup_hooks (a_hooks: CMS_HOOK_CORE_MANAGER) + do + a_hooks.subscribe_to_form_alter_hook (Current) + end + +feature -- Hook + + form_alter (a_form: CMS_FORM; a_form_data: detachable WSF_FORM_DATA; a_response: CMS_RESPONSE) + -- Hook execution on form `a_form' and its associated data `a_form_data', + -- for related response `a_response'. + local + fset: WSF_FORM_FIELD_SET + tf: WSF_FORM_TEXT_INPUT + do + if + attached a_form.id as fid and then + fid.same_string ("roccms-user-view") + then + if + attached a_response.user as u and then + attached a_response.api.user_api as l_user_profile_api and then + attached l_user_profile_api.user_profile (u) as l_profile and then + not l_profile.is_empty + then + create fset.make + fset.set_legend ("User-Profile") + a_form.extend (fset) + across + l_profile as ic + loop + create tf.make_with_text (ic.key.to_string_32, ic.item) + tf.set_label (ic.key.to_string_32) + a_form.extend (tf) + end + end + end + end + + note copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" diff --git a/src/service/user/cms_user_api.e b/src/modules/core/cms_user_api.e similarity index 68% rename from src/service/user/cms_user_api.e rename to src/modules/core/cms_user_api.e index a8078d3..72f0b22 100644 --- a/src/service/user/cms_user_api.e +++ b/src/modules/core/cms_user_api.e @@ -8,12 +8,34 @@ class inherit CMS_MODULE_API + redefine + initialize + end + + CMS_USER_PROFILE_API + redefine + initialize + end REFACTORING_HELPER create make +feature {NONE} -- Initialization + + initialize + do + Precursor {CMS_MODULE_API} + Precursor {CMS_USER_PROFILE_API} + user_storage := storage + end + +feature -- Storage + + user_storage: CMS_USER_STORAGE_I + -- User storage. + feature -- Validation is_valid_username (a_name: READABLE_STRING_32): BOOLEAN @@ -87,43 +109,53 @@ feature -- Access: user user_by_id (a_id: like {CMS_USER}.id): detachable CMS_USER -- User by id `a_id', if any. do - Result := storage.user_by_id (a_id) + Result := user_storage.user_by_id (a_id) end user_by_name (a_username: READABLE_STRING_GENERAL): detachable CMS_USER -- User by name `a_user_name', if any. do - Result := storage.user_by_name (a_username) + Result := user_storage.user_by_name (a_username) + end + + user_by_id_or_name (a_uid: READABLE_STRING_GENERAL): detachable CMS_USER + -- User by id or name `a_uid`, if any. + do + if a_uid.is_integer_64 then + Result := user_by_id (a_uid.to_integer_64) + else + Result := user_by_name (a_uid) + end end user_by_email (a_email: READABLE_STRING_GENERAL): detachable CMS_USER -- User by email `a_email', if any. do - Result := storage.user_by_email (a_email) + Result := user_storage.user_by_email (a_email) end user_by_activation_token (a_token: READABLE_STRING_32): detachable CMS_USER -- User by activation token `a_token'. do - Result := storage.user_by_activation_token (a_token) + Result := user_storage.user_by_activation_token (a_token) end user_by_password_token (a_token: READABLE_STRING_32): detachable CMS_USER -- User by password token `a_token'. do - Result := storage.user_by_password_token (a_token) + Result := user_storage.user_by_password_token (a_token) end users_count: INTEGER -- Number of users. do - Result := storage.users_count + Result := user_storage.users_count end recent_users (params: CMS_DATA_QUERY_PARAMETERS): ITERABLE [CMS_USER] -- List of the `a_rows' most recent users starting from `a_offset'. do - Result := storage.recent_users (params.offset.to_integer_32, params.size.to_integer_32) + Result := user_storage.recent_users (params.offset.to_integer_32, params.size.to_integer_32) end admin_user: detachable CMS_USER @@ -149,8 +181,8 @@ feature -- Change User if attached a_user.email as l_email then - storage.new_user (a_user) - error_handler.append (storage.error_handler) + user_storage.new_user (a_user) + error_handler.append (user_storage.error_handler) else error_handler.add_custom_error (0, "bad new user request", "Missing password or email to create new user!") end @@ -164,8 +196,8 @@ feature -- Change User user_by_name (a_new_username) = Void do reset_error - storage.update_username (a_user, a_new_username) - error_handler.append (storage.error_handler) + user_storage.update_username (a_user, a_new_username) + error_handler.append (user_storage.error_handler) end update_user (a_user: CMS_USER) @@ -174,8 +206,8 @@ feature -- Change User has_id: a_user.has_id do reset_error - storage.update_user (a_user) - error_handler.append (storage.error_handler) + user_storage.update_user (a_user) + error_handler.append (user_storage.error_handler) end delete_user (a_user: CMS_USER) @@ -184,8 +216,8 @@ feature -- Change User has_id: a_user.has_id do reset_error - storage.delete_user (a_user) - error_handler.append (storage.error_handler) + user_storage.delete_user (a_user) + error_handler.append (user_storage.error_handler) end feature -- Status report @@ -193,7 +225,7 @@ feature -- Status report is_valid_credential (a_auth_login, a_auth_password: READABLE_STRING_32): BOOLEAN -- Is the credentials `a_auth_login' and `a_auth_password' valid? do - Result := storage.is_valid_credential (a_auth_login, a_auth_password) + Result := user_storage.is_valid_credential (a_auth_login, a_auth_password) end user_has_permission (a_user: detachable CMS_USER; a_permission: detachable READABLE_STRING_GENERAL): BOOLEAN @@ -229,7 +261,7 @@ feature -- Status report if l_roles = Void then -- Fill user with its roles. create {ARRAYED_LIST [CMS_USER_ROLE]} l_roles.make (0) - l_roles := storage.user_roles_for (a_user) + l_roles := user_storage.user_roles_for (a_user) end Result := l_roles end @@ -262,13 +294,13 @@ feature -- User roles. user_role_by_id (a_id: like {CMS_USER_ROLE}.id): detachable CMS_USER_ROLE -- Retrieve a `Role' represented by an id `a_id' if any. do - Result := storage.user_role_by_id (a_id) + Result := user_storage.user_role_by_id (a_id) end user_role_by_name (a_name: READABLE_STRING_GENERAL): detachable CMS_USER_ROLE -- Retrieve a `Role' represented by a name `a_name' if any. do - Result := storage.user_role_by_name (a_name) + Result := user_storage.user_role_by_name (a_name) end role_permissions: HASH_TABLE [LIST [READABLE_STRING_8], STRING_8] @@ -278,12 +310,16 @@ feature -- User roles. do create Result.make (cms_api.enabled_modules.count + 1) - l_used_permissions := storage.role_permissions + l_used_permissions := user_storage.role_permissions across cms_api.enabled_modules as ic loop lst := ic.item.permissions - if attached {CMS_ADMINISTRABLE} ic.item as adm then + if + attached {CMS_ADMINISTRABLE} ic.item as adm and then + attached adm.module_administration.permissions as adm_permissions and then + not adm_permissions.is_empty + then create {ARRAYED_LIST [READABLE_STRING_8]} l_full_lst.make (lst.count) l_full_lst.compare_objects -- l_full_lst.append (lst) @@ -294,14 +330,38 @@ feature -- User roles. l_full_lst.extend (lst_ic.item) end end - -- l_full_lst.append (adm.module_administration.permissions) - lst := adm.module_administration.permissions + -- l_full_lst.append (adm_permissions) + across + adm_permissions as lst_ic + loop + if not l_full_lst.has (lst_ic.item) then + l_full_lst.extend (lst_ic.item) + end + end + lst := l_full_lst + end + if + attached {CMS_WITH_WEBAPI} ic.item as wapi and then + attached wapi.module_webapi.permissions as wapi_permissions and then + not wapi_permissions.is_empty + then + create {ARRAYED_LIST [READABLE_STRING_8]} l_full_lst.make (lst.count) + l_full_lst.compare_objects + -- l_full_lst.append (lst) across lst as lst_ic loop if not l_full_lst.has (lst_ic.item) then l_full_lst.extend (lst_ic.item) end + end + -- l_full_lst.append (wapi_permissions) + across + wapi_permissions as lst_ic + loop + if not l_full_lst.has (lst_ic.item) then + l_full_lst.extend (lst_ic.item) + end end lst := l_full_lst end @@ -331,7 +391,7 @@ feature -- User roles. roles: LIST [CMS_USER_ROLE] -- List of possible roles. do - Result := storage.user_roles + Result := user_storage.user_roles end effective_roles: LIST [CMS_USER_ROLE] @@ -340,7 +400,7 @@ feature -- User roles. l_roles: like roles r: CMS_USER_ROLE do - l_roles := storage.user_roles + l_roles := user_storage.user_roles create {ARRAYED_LIST [CMS_USER_ROLE]} Result.make (l_roles.count) across l_roles as ic @@ -357,7 +417,7 @@ feature -- User roles. roles_count: INTEGER -- Number of roles do - Result := storage.user_roles.count + Result := user_storage.user_roles.count end feature -- Change User role @@ -365,31 +425,31 @@ feature -- Change User role save_user_role (a_user_role: CMS_USER_ROLE) do reset_error - storage.save_user_role (a_user_role) - error_handler.append (storage.error_handler) + user_storage.save_user_role (a_user_role) + error_handler.append (user_storage.error_handler) end unassign_role_from_user (a_role: CMS_USER_ROLE; a_user: CMS_USER; ) -- Unassign user_role `a_role' to user `a_user'. do reset_error - storage.unassign_role_from_user (a_role, a_user) - error_handler.append (storage.error_handler) + user_storage.unassign_role_from_user (a_role, a_user) + error_handler.append (user_storage.error_handler) end assign_role_to_user (a_role: CMS_USER_ROLE; a_user: CMS_USER; ) -- Assign user_role `a_role' to user `a_user'. do reset_error - storage.assign_role_to_user (a_role, a_user) - error_handler.append (storage.error_handler) + user_storage.assign_role_to_user (a_role, a_user) + error_handler.append (user_storage.error_handler) end delete_role (a_role: CMS_USER_ROLE) do reset_error - storage.delete_role (a_role) - error_handler.append (storage.error_handler) + user_storage.delete_role (a_role) + error_handler.append (user_storage.error_handler) end feature -- User Activation @@ -397,7 +457,7 @@ feature -- User Activation new_activation (a_token: READABLE_STRING_32; a_id: INTEGER_64) -- Save activation token `a_token', for the user with the id `a_id'. do - storage.save_activation (a_token, a_id) + user_storage.save_activation (a_token, a_id) end feature -- User Password Recovery @@ -405,13 +465,13 @@ feature -- User Password Recovery new_password (a_token: READABLE_STRING_32; a_id: INTEGER_64) -- Save password token `a_token', for the user with the id `a_id'. do - storage.save_password (a_token, a_id) + user_storage.save_password (a_token, a_id) end remove_password (a_token: READABLE_STRING_32) - -- Remove password token `a_token', from the storage. + -- Remove password token `a_token', from the user_storage. do - storage.remove_password (a_token) + user_storage.remove_password (a_token) end feature -- User status @@ -423,7 +483,7 @@ feature -- User status -- The user is active Trashed: INTEGER = -1 - -- The user is trashed (soft delete), ready to be deleted/destroyed from storage. + -- The user is trashed (soft delete), ready to be deleted/destroyed from user_storage. feature -- Access - Temp User @@ -431,36 +491,36 @@ feature -- Access - Temp User -- Number of pending users. --! to be accepted or rehected do - Result := storage.temp_users_count + Result := user_storage.temp_users_count end temp_user_by_name (a_username: READABLE_STRING_GENERAL): detachable CMS_USER -- User by name `a_user_name', if any. do - Result := storage.temp_user_by_name (a_username.as_string_32) + Result := user_storage.temp_user_by_name (a_username.as_string_32) end temp_user_by_email (a_email: READABLE_STRING_8): detachable CMS_USER -- User by email `a_email', if any. do - Result := storage.temp_user_by_email (a_email) + Result := user_storage.temp_user_by_email (a_email) end temp_user_by_activation_token (a_token: READABLE_STRING_32): detachable CMS_USER -- User by activation token `a_token'. do - Result := storage.temp_user_by_activation_token (a_token) + Result := user_storage.temp_user_by_activation_token (a_token) end temp_recent_users (params: CMS_DATA_QUERY_PARAMETERS): ITERABLE [CMS_TEMP_USER] -- List of the `a_rows' most recent users starting from `a_offset'. do - Result := storage.temp_recent_users (params.offset.to_integer_32, params.size.to_integer_32) + Result := user_storage.temp_recent_users (params.offset.to_integer_32, params.size.to_integer_32) end token_by_temp_user_id (a_id: like {CMS_USER}.id): detachable STRING do - Result := storage.token_by_temp_user_id (a_id) + Result := user_storage.token_by_temp_user_id (a_id) end feature -- Change Temp User @@ -477,8 +537,8 @@ feature -- Change Temp User attached a_temp_user.salt as l_salt and then attached a_temp_user.email as l_email then - storage.new_user_from_temp_user (a_temp_user) - error_handler.append (storage.error_handler) + user_storage.new_user_from_temp_user (a_temp_user) + error_handler.append (user_storage.error_handler) else error_handler.add_custom_error (0, "bad new user request", "Missing password or email to create new user!") end @@ -495,17 +555,17 @@ feature -- Change Temp User attached a_temp_user.password as l_password and then attached a_temp_user.email as l_email then - storage.new_temp_user (a_temp_user) - error_handler.append (storage.error_handler) + user_storage.new_temp_user (a_temp_user) + error_handler.append (user_storage.error_handler) else error_handler.add_custom_error (0, "bad new user request", "Missing password or email to create new user!") end end remove_activation (a_token: READABLE_STRING_32) - -- Remove activation token `a_token', from the storage. + -- Remove activation token `a_token', from the user_storage. do - storage.remove_activation (a_token) + user_storage.remove_activation (a_token) end delete_temp_user (a_temp_user: CMS_TEMP_USER) @@ -514,10 +574,38 @@ feature -- Change Temp User has_id: a_temp_user.has_id do reset_error - storage.delete_temp_user (a_temp_user) - error_handler.append (storage.error_handler) + user_storage.delete_temp_user (a_temp_user) + error_handler.append (user_storage.error_handler) end +--feature -- Access: User profile + +-- user_profile (a_user: CMS_USER): detachable CMS_USER_PROFILE +-- -- User profile for `a_user'. +-- require +-- valid_user: a_user.has_id +-- do +-- Result := user_profile_storage.user_profile (a_user) +-- end + +-- user_profile_item (a_item_name: READABLE_STRING_GENERAL; a_user: CMS_USER): detachable READABLE_STRING_32 +-- -- User profile item `a_item_name` for `a_user`. +-- require +-- valid_user: a_user.has_id +-- do +-- Result := user_profile_storage.user_profile_item (a_user, a_item_name) +-- end + +--feature -- Change: User profile + +-- save_user_profile (a_user: CMS_USER; a_user_profile: CMS_USER_PROFILE) +-- -- Save `a_user' profile `a_user_profile'. +-- require +-- valid_user: a_user.has_id +-- do +-- user_profile_storage.save_user_profile (a_user, a_user_profile) +-- end + note copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" diff --git a/src/modules/core/cms_user_profile_api.e b/src/modules/core/cms_user_profile_api.e new file mode 100644 index 0000000..c4684cd --- /dev/null +++ b/src/modules/core/cms_user_profile_api.e @@ -0,0 +1,87 @@ +note + description: "API to handle user profiles." + date: "$Date$" + revision: "$Revision$" + +class + CMS_USER_PROFILE_API + +inherit + CMS_MODULE_API + redefine + initialize + end + + REFACTORING_HELPER + +create + make + +feature {NONE} -- Initialization + + initialize + -- + do + Precursor + -- Storage initialization + if attached cms_api.storage.as_sql_storage as l_storage_sql then + create {CMS_USER_PROFILE_STORAGE_SQL} user_profile_storage.make (l_storage_sql) + else + -- FIXME: in case of NULL storage, should Current be disabled? + create {CMS_USER_PROFILE_STORAGE_NULL} user_profile_storage + end + end + +feature {CMS_MODULE} -- Access nodes storage. + + user_profile_storage: CMS_USER_PROFILE_STORAGE_I + +feature -- Access: profile + + user_profile (a_user: CMS_USER): detachable CMS_USER_PROFILE + -- User profile for `a_user'. + require + valid_user: a_user.has_id + do + Result := user_profile_storage.user_profile (a_user) + end + + user_profile_item (a_item_name: READABLE_STRING_GENERAL; a_user: CMS_USER): detachable READABLE_STRING_32 + -- User profile item `a_item_name` for `a_user`. + require + valid_user: a_user.has_id + do + Result := user_profile_storage.user_profile_item (a_user, a_item_name) + end + + users_with_profile_item (a_item_name: READABLE_STRING_GENERAL; a_value: detachable READABLE_STRING_GENERAL): detachable LIST [CMS_USER] + -- Users having a profile item `a_item_name:a_value`. + -- Note: if `a_value` is Void, return users having a profile item named `a_item_name`. + require + not a_item_name.is_whitespace + do + Result := user_profile_storage.users_with_profile_item (a_item_name, a_value) + end + +feature -- Change: profile + + save_user_profile (a_user: CMS_USER; a_user_profile: CMS_USER_PROFILE) + -- Save `a_user' profile `a_user_profile'. + require + valid_user: a_user.has_id + do + user_profile_storage.save_user_profile (a_user, a_user_profile) + end + + save_user_profile_item (a_user: CMS_USER; a_user_profile_name, a_user_profile_value: READABLE_STRING_GENERAL) + -- Save `a_user' profile item `a_user_profile_name=a_user_profile_value`. + require + valid_user: a_user.has_id + do + user_profile_storage.save_user_profile_item (a_user, a_user_profile_name, a_user_profile_value) + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/modules/auth/cms_user_handler.e b/src/modules/core/handler/user/cms_user_handler.e similarity index 67% rename from modules/auth/cms_user_handler.e rename to src/modules/core/handler/user/cms_user_handler.e index d54b431..49e64d9 100644 --- a/modules/auth/cms_user_handler.e +++ b/src/modules/core/handler/user/cms_user_handler.e @@ -57,15 +57,21 @@ feature -- execute feature -- Query - user_id_path_parameter (req: WSF_REQUEST): INTEGER_64 - -- User id passed as path parameter for request `req'. + user_path_parameter (req: WSF_REQUEST): detachable CMS_USER + -- User id (uid or username) passed as path parameter for request `req'. local s: STRING + l_uid: INTEGER_64 do if attached {WSF_STRING} req.path_parameter ("uid") as p_nid then s := p_nid.value if s.is_integer_64 then - Result := s.to_integer_64 + l_uid := s.to_integer_64 + if l_uid > 0 then + Result := api.user_api.user_by_id (l_uid) + end + else + Result := api.user_api.user_by_name (s) end end end @@ -76,28 +82,23 @@ feature -- HTTP Methods -- local l_user: detachable CMS_USER - l_uid: INTEGER_64 - view_response: CMS_USER_VIEW_RESPONSE do if api.has_permission ("view user") then -- Display existing node - l_uid := user_id_path_parameter (req) - if l_uid > 0 then - l_user := api.user_api.user_by_id (l_uid) - if - l_user /= Void - then - create view_response.make (req, res, api) - view_response.execute - else - send_not_found (req, res) - end + l_user := user_path_parameter (req) + if + l_user /= Void + then + (create {CMS_USER_VIEW_RESPONSE}.make_with_user (l_user, req, res, api)).execute else - send_bad_request (req, res) + send_not_found (req, res) end else send_access_denied (req, res) end end +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/modules/auth/cms_user_view_response.e b/src/modules/core/handler/user/cms_user_view_response.e similarity index 55% rename from modules/auth/cms_user_view_response.e rename to src/modules/core/handler/user/cms_user_view_response.e index 612180c..c8a00c7 100644 --- a/modules/auth/cms_user_view_response.e +++ b/src/modules/core/handler/user/cms_user_view_response.e @@ -10,7 +10,19 @@ inherit CMS_RESPONSE create - make + make_with_user + +feature {NONE} -- Initialization + + make_with_user (u: CMS_USER; req: WSF_REQUEST; res: WSF_RESPONSE; a_api: like api) + do + make (req, res, a_api) + associated_user := u + end + +feature -- Access + + associated_user: CMS_USER feature -- Query @@ -33,16 +45,11 @@ feature -- Process -- Computed response message. local b: STRING_8 - uid: INTEGER_64 - user_api: CMS_USER_API f: CMS_FORM do - user_api := api.user_api create b.make_empty - uid := user_id_path_parameter (request) if - uid > 0 and then - attached user_api.user_by_id (uid) as l_user + attached associated_user as l_user then if api.has_permission ("view user") @@ -67,7 +74,6 @@ feature -- Process Edit th: WSF_FORM_HIDDEN_INPUT do create Result.make (a_url, a_name) - create th.make ("user-id") if a_user /= Void then th.set_text_value (a_user.id.out) @@ -79,12 +85,14 @@ feature -- Process Edit populate_form (Result, a_user) end - populate_form (a_form: WSF_FORM; a_user: detachable CMS_USER) + populate_form (a_form: CMS_FORM; a_user: detachable CMS_USER) -- Fill the web form `a_form' with data from `a_node' if set, -- and apply this to content type `a_content_type'. local ti: WSF_FORM_TEXT_INPUT fs: WSF_FORM_FIELD_SET + l_new_access_token_form: WSF_FORM + l_access_token: detachable READABLE_STRING_32 do if a_user /= Void then create fs.make @@ -97,7 +105,33 @@ feature -- Process Edit ti.set_is_readonly (True) fs.extend (ti) a_form.extend (fs) + if api.setup.webapi_enabled then + create fs.make + fs.set_legend ("Web API") + l_access_token := api.user_api.user_profile_item ("access_token", a_user) + if l_access_token /= Void then + create ti.make_with_text ("api_access_token", a_user.name) + ti.set_text_value (l_access_token) + ti.set_label ("Access Token") + ti.set_is_readonly (True) + fs.extend (ti) + end + + create l_new_access_token_form.make (api.webapi_path ("access_token"), Void) + l_new_access_token_form.set_method_post + if l_access_token /= Void then + l_new_access_token_form.extend (create {WSF_FORM_SUBMIT_INPUT}.make_with_text ("access_token_op", "Refresh Access Token")) + else + l_new_access_token_form.extend (create {WSF_FORM_SUBMIT_INPUT}.make_with_text ("access_token_op", "Create Access Token")) + end + l_new_access_token_form.extend (create {WSF_FORM_HIDDEN_INPUT}.make_with_text ("destination", request.percent_encoded_path_info)) + a_form.put_widget_after_form (l_new_access_token_form) + a_form.extend (fs) + end end end +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/src/persistence/core/cms_core_storage_i.e b/src/modules/core/persistence/core/cms_core_storage_i.e similarity index 100% rename from src/persistence/core/cms_core_storage_i.e rename to src/modules/core/persistence/core/cms_core_storage_i.e diff --git a/src/persistence/core/cms_core_storage_sql_i.e b/src/modules/core/persistence/core/cms_core_storage_sql_i.e similarity index 100% rename from src/persistence/core/cms_core_storage_sql_i.e rename to src/modules/core/persistence/core/cms_core_storage_sql_i.e diff --git a/src/persistence/user/cms_user_storage_i.e b/src/modules/core/persistence/user/cms_user_storage_i.e similarity index 100% rename from src/persistence/user/cms_user_storage_i.e rename to src/modules/core/persistence/user/cms_user_storage_i.e diff --git a/src/persistence/user/cms_user_storage_null.e b/src/modules/core/persistence/user/cms_user_storage_null.e similarity index 100% rename from src/persistence/user/cms_user_storage_null.e rename to src/modules/core/persistence/user/cms_user_storage_null.e diff --git a/src/persistence/user/cms_user_storage_sql_i.e b/src/modules/core/persistence/user/cms_user_storage_sql_i.e similarity index 99% rename from src/persistence/user/cms_user_storage_sql_i.e rename to src/modules/core/persistence/user/cms_user_storage_sql_i.e index f3f2cf4..d34e1a8 100644 --- a/src/persistence/user/cms_user_storage_sql_i.e +++ b/src/modules/core/persistence/user/cms_user_storage_sql_i.e @@ -800,7 +800,7 @@ feature -- Change: User activation sql_begin_transaction write_information_log (generator + ".save_activation") create l_utc_date.make_now_utc - create l_parameters.make (2) + create l_parameters.make (3) l_parameters.put (a_token, "token") l_parameters.put (a_id, "uid") l_parameters.put (l_utc_date, "utc_date") diff --git a/src/modules/core/persistence/user_profile/cms_user_profile_storage_i.e b/src/modules/core/persistence/user_profile/cms_user_profile_storage_i.e new file mode 100644 index 0000000..21ead62 --- /dev/null +++ b/src/modules/core/persistence/user_profile/cms_user_profile_storage_i.e @@ -0,0 +1,66 @@ +note + description: "Interface for accessing user profile contents from the database." + date: "$Date$" + revision: "$Revision$" + +deferred class + CMS_USER_PROFILE_STORAGE_I + +feature -- Error Handling + + error_handler: ERROR_HANDLER + -- Error handler. + deferred + end + +feature -- Access + + user_profile (a_user: CMS_USER): detachable CMS_USER_PROFILE + -- User profile for `a_user'. + require + has_id: a_user.has_id + deferred + end + + user_profile_item (a_user: CMS_USER; a_item_name: READABLE_STRING_GENERAL): detachable READABLE_STRING_32 + require + valid_user: a_user.has_id + do + if attached user_profile (a_user) as pf then + Result := pf.item (a_item_name) + end + end + + users_with_profile_item (a_item_name: READABLE_STRING_GENERAL; a_value: detachable READABLE_STRING_GENERAL): detachable LIST [CMS_USER] + -- Users having a profile item `a_item_name:a_value`. + -- Note: if `a_value` is Void, return users having a profile item named `a_item_name`. + deferred + end + +feature -- Change + + save_user_profile (a_user: CMS_USER; a_profile: CMS_USER_PROFILE) + -- Save user profile `a_profile' for `a_user'. + require + user_has_id: a_user.has_id + deferred + end + + save_user_profile_item (a_user: CMS_USER; a_profile_item_name: READABLE_STRING_GENERAL; a_profile_item_value: READABLE_STRING_GENERAL) + require + user_has_id: a_user.has_id + local + pf: detachable CMS_USER_PROFILE + do + pf := user_profile (a_user) + if pf = Void then + create pf.make + end + pf.force (a_profile_item_value, a_profile_item_name) + save_user_profile (a_user, pf) + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/modules/core/persistence/user_profile/cms_user_profile_storage_null.e b/src/modules/core/persistence/user_profile/cms_user_profile_storage_null.e new file mode 100644 index 0000000..19c92b5 --- /dev/null +++ b/src/modules/core/persistence/user_profile/cms_user_profile_storage_null.e @@ -0,0 +1,42 @@ +note + description: "Summary description for {CMS_USER_PROFILE_STORAGE_NULL}." + date: "$Date$" + revision: "$Revision$" + +class + CMS_USER_PROFILE_STORAGE_NULL + +inherit + CMS_USER_PROFILE_STORAGE_I + +feature -- Error handler + + error_handler: ERROR_HANDLER + -- Error handler. + do + create Result.make + end + +feature -- Access + + user_profile (a_user: CMS_USER): detachable CMS_USER_PROFILE + -- + do + end + + users_with_profile_item (a_item_name: READABLE_STRING_GENERAL; a_value: detachable READABLE_STRING_GENERAL): detachable LIST [CMS_USER] + -- + do + end + +feature -- Change + + save_user_profile (a_user: CMS_USER; a_profile: CMS_USER_PROFILE) + -- + do + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/modules/core/persistence/user_profile/cms_user_profile_storage_sql.e b/src/modules/core/persistence/user_profile/cms_user_profile_storage_sql.e new file mode 100644 index 0000000..d4ea036 --- /dev/null +++ b/src/modules/core/persistence/user_profile/cms_user_profile_storage_sql.e @@ -0,0 +1,212 @@ +note + description: "Interface for accessing user profile contents from SQL database." + date: "$Date$" + revision: "$Revision$" + +class + CMS_USER_PROFILE_STORAGE_SQL + +inherit + CMS_USER_PROFILE_STORAGE_I + redefine + user_profile_item, + save_user_profile_item + end + + CMS_PROXY_STORAGE_SQL + + CMS_STORAGE_SQL_I + +create + make + +feature -- Access + + user_profile_item (a_user: CMS_USER; a_item_name: READABLE_STRING_GENERAL): detachable READABLE_STRING_32 + -- User profile for `a_user'. + local + l_parameters: STRING_TABLE [detachable ANY] + do + reset_error + create l_parameters.make (2) + l_parameters.put (a_user.id, "uid") + l_parameters.put (a_item_name, "key") + sql_query (sql_select_user_profile_item, l_parameters) + if not has_error then + Result := sql_read_string_32 (2) + end + sql_finalize + end + + user_profile (a_user: CMS_USER): detachable CMS_USER_PROFILE + -- User profile for `a_user'. + local + l_parameters: STRING_TABLE [detachable ANY] + do + reset_error + create l_parameters.make (1) + l_parameters.put (a_user.id, "uid") + sql_query (sql_select_user_profile_items, l_parameters) + if not has_error then + create Result.make + from + sql_start + until + sql_after or has_error + loop + if + attached sql_read_string_32 (1) as l_key and + attached sql_read_string_32 (2) as l_val + then + Result.force (l_val, l_key) + end + sql_forth + end + end + sql_finalize + end + + users_with_profile_item (a_item_name: READABLE_STRING_GENERAL; a_value: detachable READABLE_STRING_GENERAL): detachable LIST [CMS_USER] + -- Users having a profile item `a_item_name:a_value`. + -- Note: if `a_value` is Void, return users having a profile item named `a_item_name`. + local + l_parameters: STRING_TABLE [detachable ANY] + l_uids: ARRAYED_LIST [INTEGER_64] + do + reset_error + create l_parameters.make (2) + l_parameters.put (a_item_name, "key") + if a_value = Void then + sql_query (sql_select_users_with_profile_item_named, l_parameters) + else + l_parameters.put (a_value, "value") + sql_query (sql_select_users_with_profile_item, l_parameters) + end + if not has_error then + create l_uids.make (0) + from + sql_start + until + sql_after or has_error + loop + if + attached sql_read_integer_64 (1) as l_uid and then + l_uid > 0 + then + l_uids.force (l_uid) + end + sql_forth + end + end + sql_finalize + if + not has_error and + l_uids /= Void and + attached api as l_cms_api + then + create {ARRAYED_LIST [CMS_USER]} Result.make (l_uids.count) + across + l_uids as ic + loop + if attached l_cms_api.user_api.user_by_id (ic.item) as u then + Result.force (u) + else + check known_user: False end + end + end + end + end + +feature -- Change + + save_user_profile_item (a_user: CMS_USER; a_item_name: READABLE_STRING_GENERAL; a_item_value: READABLE_STRING_GENERAL) + -- Save user profile item `a_item_name:a_item_value` for `a_user'. + local + l_parameters: STRING_TABLE [detachable ANY] + do + create l_parameters.make (3) + l_parameters.put (a_user.id, "uid") + l_parameters.put (a_item_name, "key") + l_parameters.put (a_item_value, "value") + + reset_error + if user_profile_item (a_user, a_item_name) = Void then + sql_insert (sql_insert_user_profile_item, l_parameters) + else + sql_modify (sql_update_user_profile_item, l_parameters) + end + sql_finalize + end + + save_user_profile (a_user: CMS_USER; a_profile: CMS_USER_PROFILE) + -- Save user profile `a_profile' for `a_user'. + local + l_parameters: STRING_TABLE [detachable ANY] + p: detachable CMS_USER_PROFILE + l_item: like user_profile_item + l_is_new: BOOLEAN + l_has_diff: BOOLEAN + do + p := user_profile (a_user) + + create l_parameters.make (3) + + reset_error + across + a_profile as ic + until + has_error + loop + l_item := ic.item + -- No previous profile, or no item with same name, or same value + l_has_diff := True + if p = Void then + l_is_new := True + elseif p.has_key (ic.key) then + l_is_new := False + l_has_diff := attached p.item (ic.key) as l_prev_item and then not l_prev_item.same_string (l_item) + else + l_is_new := True + end + if l_has_diff then + l_parameters.put (a_user.id, "uid") + l_parameters.put (ic.key, "key") + l_parameters.put (l_item, "value") + + if l_is_new then + sql_insert (sql_insert_user_profile_item, l_parameters) + else + sql_modify (sql_update_user_profile_item, l_parameters) + end + l_parameters.wipe_out + end + end + sql_finalize + end + +feature {NONE} -- Queries + + sql_select_user_profile_items: STRING = "SELECT key, value FROM user_profiles WHERE uid=:uid;" + -- user profile items for :uid; + + sql_select_user_profile_item: STRING = "SELECT key, value FROM user_profiles WHERE uid=:uid AND key=:key" + -- user profile items for :uid; + + sql_select_users_with_profile_item: STRING = "SELECT uid FROM user_profiles WHERE key=:key and value=:value" + -- users with profile item named :key and value :value; + + sql_select_users_with_profile_item_named: STRING = "SELECT uid FROM user_profiles WHERE key=:key" + -- users with profile item named :key; + + sql_insert_user_profile_item: STRING = "INSERT INTO user_profiles (uid, key, value) VALUES (:uid, :key, :value);" + -- new user profile item for :uid; + + sql_update_user_profile_item: STRING = "UPDATE user_profiles SET value = :value WHERE uid = :uid AND key = :key;" + -- user profile items for :uid; + + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end + diff --git a/src/modules/core/webapi/cms_access_token_webapi_auth_filter.e b/src/modules/core/webapi/cms_access_token_webapi_auth_filter.e new file mode 100644 index 0000000..e25fddd --- /dev/null +++ b/src/modules/core/webapi/cms_access_token_webapi_auth_filter.e @@ -0,0 +1,39 @@ +note + description: "Summary description for {CMS_CORE_ACCESS_TOKEN_WEBAPI_AUTH_FILTER}." + date: "$Date$" + revision: "$Revision$" + +class + CMS_ACCESS_TOKEN_WEBAPI_AUTH_FILTER + +inherit + CMS_WEBAPI_AUTH_FILTER + +create + make + +feature -- Basic operations + + execute (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Execute the filter. + local + tok: READABLE_STRING_GENERAL + do + if + attached req.http_authorization as l_auth and then + l_auth.starts_with_general ("Bearer ") + then + tok := l_auth.substring (8, l_auth.count) + if attached api.user_api.users_with_profile_item ("access_token", tok) as lst then + if lst.count = 1 then + api.set_user (lst.first) + end + end + end + execute_next (req, res) + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/modules/core/webapi/cms_access_token_webapi_handler.e b/src/modules/core/webapi/cms_access_token_webapi_handler.e new file mode 100644 index 0000000..5b8c79c --- /dev/null +++ b/src/modules/core/webapi/cms_access_token_webapi_handler.e @@ -0,0 +1,182 @@ +note + description: "Summary description for {CMS_ACCESS_TOKEN_WEBAPI_HANDLER}." + date: "$Date$" + revision: "$Revision$" + +class + CMS_ACCESS_TOKEN_WEBAPI_HANDLER + +inherit + CMS_WEBAPI_HANDLER + + WSF_URI_TEMPLATE_HANDLER + +create + make + +feature -- Execution + + execute (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Execute handler for `req' and respond in `res'. + local + l_uid: READABLE_STRING_GENERAL + do + if attached {WSF_STRING} req.path_parameter ("uid") as p_uid then + l_uid := p_uid.value + if req.is_post_request_method then + post_access_token (l_uid, req, res) + elseif req.is_get_request_method then + get_access_token (l_uid, req, res) + else + send_bad_request (Void, req, res) + end + else + send_bad_request ("Missing {uid} parameter", req, res) + end + end + +feature -- Helper + + user_by_uid (a_uid: READABLE_STRING_GENERAL): detachable CMS_USER + do + Result := api.user_api.user_by_id_or_name (a_uid) + end + +feature -- Request execution + + get_access_token (a_uid: READABLE_STRING_GENERAL; req: WSF_REQUEST; res: WSF_RESPONSE) + -- Execute handler for `req' and respond in `res'. + local + rep: HM_WEBAPI_RESPONSE + do + if attached user_by_uid (a_uid) as l_user then + if attached api.user as u then + if u.same_as (l_user) or api.user_api.is_admin_user (u) then + rep := new_access_token_webapi_response (l_user, user_access_token (l_user), req, res) + if attached {WSF_STRING} req.item ("destination") as dest then + rep.set_redirection (dest.url_encoded_value) + end + rep.execute + else + -- Only admin, or current user can see its access_token! + send_access_denied (Void, req, res) + end + else + send_access_denied (Void, req, res) + end + else + send_not_found ("User not found", req, res) + end + end + + post_access_token (a_uid: READABLE_STRING_GENERAL; req: WSF_REQUEST; res: WSF_RESPONSE) + -- Execute handler for `req' and respond in `res'. + local + l_access_token: detachable READABLE_STRING_32 + rep: like new_webapi_response + do + if attached user_by_uid (a_uid) as l_user then + if attached api.user as u then + if u.same_as (l_user) or api.user_api.is_admin_user (u) then + if attached req.path_parameter ("application") then + + end + -- l_access_token := user_access_token (l_user) + l_access_token := new_key (40) + + -- if l_access_token /= Void then + -- l_access_token := "Updated-" + (create {UUID_GENERATOR}).generate_uuid.out + -- else + -- l_access_token := "New-" + (create {UUID_GENERATOR}).generate_uuid.out + -- end + set_user_access_token (l_user, l_access_token) + + rep := new_access_token_webapi_response (l_user, l_access_token, req, res) + if attached {WSF_STRING} req.item ("destination") as dest then + rep.set_redirection (dest.url_encoded_value) + end + rep.execute + else + -- Only admin, or current user can create the user access_token! + send_access_denied (Void, req, res) + end + else + send_access_denied (Void, req, res) + end + else + send_not_found ("User not found", req, res) + end + end + +feature {NONE} -- Implementation + + user_access_token (a_user: CMS_USER): detachable READABLE_STRING_8 + do + if + attached api.user_api.user_profile_item ("access_token", a_user) as l_access_token and then + not l_access_token.is_whitespace and then + l_access_token.is_valid_as_string_8 + then + Result := l_access_token.to_string_8 + else +-- Result := new_key (40) + end + end + + set_user_access_token (a_user: CMS_USER; a_access_token: READABLE_STRING_GENERAL) + do + api.user_api.save_user_profile_item (a_user, "access_token", a_access_token) + end + + new_access_token_webapi_response (a_user: CMS_USER; a_access_token: detachable READABLE_STRING_GENERAL; req: WSF_REQUEST; res: WSF_RESPONSE): like new_webapi_response + local + tb: STRING_TABLE [detachable ANY] + do + Result := new_webapi_response (req, res) + if a_access_token /= Void then + Result.add_string_field ("access_token", a_access_token) + else + Result.add_string_field ("access_token", "NONE") + Result.add_link ("new_access_token", "user/" + a_user.id.out + "/access_token", req.percent_encoded_path_info) + end + create tb.make_equal (3) + tb.force (a_user.name, "name") + tb.force (a_user.id, "uid") + Result.add_table_iterator_field ("user", tb) + +-- Result.add_string_field ("username", a_user.name) +-- Result.add_integer_64_field ("uid", a_user.id) + Result.add_self (req.percent_encoded_path_info) + add_user_links_to (a_user, Result) + end + + new_key (len: INTEGER): STRING_8 + local + rand: RANDOM + n: INTEGER + v: NATURAL_32 + do + create rand.set_seed ((create {DATE_TIME}.make_now_utc).seconds) + rand.start + create Result.make (len) + from + n := 1 + until + n = len + loop + rand.forth + v := (rand.item \\ 16).to_natural_32 + check 0 <= v and v <= 15 end + if v < 9 then + Result.append_code (48 + v) -- 48 '0' + else + Result.append_code (97 + v - 9) -- 97 'a' + end + n := n + 1 + end + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/modules/core/webapi/cms_basic_webapi_auth_filter.e b/src/modules/core/webapi/cms_basic_webapi_auth_filter.e new file mode 100644 index 0000000..68f9d80 --- /dev/null +++ b/src/modules/core/webapi/cms_basic_webapi_auth_filter.e @@ -0,0 +1,43 @@ +note + description: "Summary description for {CMS_BASIC_WEBAPI_AUTH_FILTER}." + date: "$Date$" + revision: "$Revision$" + +class + CMS_BASIC_WEBAPI_AUTH_FILTER + +inherit + CMS_WEBAPI_AUTH_FILTER + +create + make + +feature -- Basic operations + + execute (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Execute the filter. + local + l_auth: HTTP_AUTHORIZATION + do + create l_auth.make (req.http_authorization) + if + l_auth.is_basic and then + attached l_auth.login as l_auth_login and then + attached l_auth.password as l_auth_password + then + if + api.user_api.is_valid_credential (l_auth_login, l_auth_password) and then + attached api.user_api.user_by_name (l_auth_login) as l_user + then + api.set_user (l_user) + else + -- not authenticated due to bad login or password. + end + end + execute_next (req, res) + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/modules/core/webapi/cms_core_module_webapi.e b/src/modules/core/webapi/cms_core_module_webapi.e new file mode 100644 index 0000000..f456ff4 --- /dev/null +++ b/src/modules/core/webapi/cms_core_module_webapi.e @@ -0,0 +1,57 @@ +note + description: "Summary description for {CMS_CORE_MODULE_WEBAPI}." + author: "" + date: "$Date$" + revision: "$Revision$" + +class + CMS_CORE_MODULE_WEBAPI + +inherit + CMS_MODULE_WEBAPI [CMS_CORE_MODULE] + redefine + permissions, + filters + end + +create + make + +feature -- Security + + permissions: LIST [READABLE_STRING_8] + -- List of permission ids, used by this module, and declared. + do + Result := Precursor + Result.force ("admin users") + Result.force ("view users") + end + +feature {NONE} -- Router/administration + + setup_webapi_router (a_router: WSF_ROUTER; a_api: CMS_API) + -- + local + l_root: CMS_ROOT_WEBAPI_HANDLER + do + create l_root.make (a_api) + a_router.handle ("", l_root, a_router.methods_get) + a_router.handle ("/", l_root, a_router.methods_get) + a_router.handle ("/user/{uid}/access_token", create {CMS_ACCESS_TOKEN_WEBAPI_HANDLER}.make (a_api), a_router.methods_get_post) + a_router.handle ("/user/{uid}", create {CMS_USER_WEBAPI_HANDLER}.make (a_api), a_router.methods_get) + end + +feature -- Access: filter + + filters (a_api: CMS_API): detachable LIST [WSF_FILTER] + -- Possibly list of Filter's module. + do + create {ARRAYED_LIST [WSF_FILTER]} Result.make (2) + Result.extend (create {CMS_ACCESS_TOKEN_WEBAPI_AUTH_FILTER}.make (a_api)) + Result.extend (create {CMS_BASIC_WEBAPI_AUTH_FILTER}.make (a_api)) + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/modules/core/webapi/cms_root_webapi_handler.e b/src/modules/core/webapi/cms_root_webapi_handler.e new file mode 100644 index 0000000..c905a34 --- /dev/null +++ b/src/modules/core/webapi/cms_root_webapi_handler.e @@ -0,0 +1,36 @@ +note + description: "Summary description for {CMS_ROOT_WEBAPI_HANDLER}." + date: "$Date$" + revision: "$Revision$" + +class + CMS_ROOT_WEBAPI_HANDLER + +inherit + CMS_WEBAPI_HANDLER + + WSF_URI_HANDLER + +create + make + +feature -- Execution + + execute (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Execute handler for `req' and respond in `res'. + local + rep: HM_WEBAPI_RESPONSE + do + rep := new_webapi_response (req, res) + rep.add_string_field ("site_name", api.setup.site_name) + if attached api.user as u then + add_user_links_to (u, rep) + end + rep.add_self (req.percent_encoded_path_info) + rep.execute + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/modules/core/webapi/cms_user_webapi_handler.e b/src/modules/core/webapi/cms_user_webapi_handler.e new file mode 100644 index 0000000..2de81cd --- /dev/null +++ b/src/modules/core/webapi/cms_user_webapi_handler.e @@ -0,0 +1,80 @@ +note + description: "Summary description for {CMS_USER_WEBAPI_HANDLER}." + date: "$Date$" + revision: "$Revision$" + +class + CMS_USER_WEBAPI_HANDLER + +inherit + CMS_WEBAPI_HANDLER + + WSF_URI_TEMPLATE_HANDLER + +create + make + +feature -- Execution + + execute (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Execute handler for `req' and respond in `res'. + do + if req.is_get_request_method then + execute_get (req, res) + else + send_bad_request (Void, req, res) + end + end + + execute_get (req: WSF_REQUEST; res: WSF_RESPONSE) + -- Execute handler for `req' and respond in `res'. + local + rep: HM_WEBAPI_RESPONSE + l_user: detachable CMS_USER + do + if attached api.user as u then + if attached {WSF_STRING} req.path_parameter ("uid") as p_uid then + if p_uid.is_integer then + l_user := api.user_api.user_by_id (p_uid.integer_value) + else + l_user := api.user_api.user_by_name (p_uid.value) + end +-- if l_user = Void and p_uid.is_case_insensitive_equal ("me") then +-- l_user := u +-- end + if l_user /= Void then + if l_user.same_as (u) or api.has_permissions (<<"admin users", "view users">>) then + rep := new_webapi_response (req, res) + rep.add_string_field ("uid", u.id.out) + + rep.add_string_field ("name", u.name) + if attached u.email as l_email then + rep.add_string_field ("email", l_email) + end + if attached u.profile_name as l_profile_name then + rep.add_string_field ("profile_name", l_profile_name) + end + add_user_links_to (u, rep) + else + rep := new_wepapi_error_response ("denied", req, res) + rep.set_status_code ({HTTP_STATUS_CODE}.user_access_denied) + end + else + rep := new_wepapi_error_response ("Not found", req, res) + rep.set_status_code ({HTTP_STATUS_CODE}.not_found) + end + else + rep := new_wepapi_error_response ("Bad request", req, res) + rep.set_status_code ({HTTP_STATUS_CODE}.bad_request) + end + rep.execute + else + -- FIXME: use specific Web API response! + send_access_denied (Void, req, res) + end + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/modules/cms_debug_module.e b/src/modules/debug/cms_debug_module.e similarity index 100% rename from src/modules/cms_debug_module.e rename to src/modules/debug/cms_debug_module.e diff --git a/src/persistence/sql/cms_proxy_storage_sql.e b/src/persistence/sql/cms_proxy_storage_sql.e index bf4b2f8..a81d68c 100644 --- a/src/persistence/sql/cms_proxy_storage_sql.e +++ b/src/persistence/sql/cms_proxy_storage_sql.e @@ -84,6 +84,11 @@ feature -- Operation sql_storage.sql_modify (a_sql_statement, a_params) end + sql_delete (a_sql_statement: STRING; a_params: detachable STRING_TABLE [detachable ANY]) + do + sql_storage.sql_delete (a_sql_statement, a_params) + end + feature -- Access sql_start @@ -133,6 +138,6 @@ feature -- Conversion end note - copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/src/persistence/sql/cms_storage_sql_i.e b/src/persistence/sql/cms_storage_sql_i.e index 09b4571..3231cc5 100644 --- a/src/persistence/sql/cms_storage_sql_i.e +++ b/src/persistence/sql/cms_storage_sql_i.e @@ -138,6 +138,11 @@ feature -- Operation deferred end + sql_delete (a_sql_statement: STRING; a_params: detachable STRING_TABLE [detachable ANY]) + -- Execute sql delete `a_sql_statement' with optional parameters `a_params'. + deferred + end + feature -- Helper sql_script_content (a_path: PATH): detachable STRING @@ -462,6 +467,6 @@ feature {NONE} -- Implementation end note - copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/src/service/cms_administrable.e b/src/service/cms_administrable.e index a3d9210..4458b9b 100644 --- a/src/service/cms_administrable.e +++ b/src/service/cms_administrable.e @@ -6,29 +6,8 @@ note deferred class CMS_ADMINISTRABLE -feature -- Administration - - module_administration: like administration - -- Associated administration module. - do - Result := internal_module_administration - if Result = Void then - Result := administration - internal_module_administration := Result - end - end - -feature {NONE} -- Implementation - - internal_module_administration: detachable like module_administration - -- Cached version of `module_administration`. - -feature {NONE} -- Administration - - administration: CMS_MODULE_ADMINISTRATION [CMS_MODULE] - -- Administration module. - deferred - end +inherit + CMS_WITH_MODULE_ADMINISTRATION note copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" diff --git a/src/service/cms_api.e b/src/service/cms_api.e index 4c6b215..a5a7511 100644 --- a/src/service/cms_api.e +++ b/src/service/cms_api.e @@ -54,6 +54,10 @@ feature {NONE} -- Initialize s := setup.administration_base_path administration_base_path_location := s.shared_substring (2, s.count) + -- Webapi backend + s := setup.webapi_base_path + webapi_base_path_location := s.shared_substring (2, s.count) + -- Initialize contents. initialize_content_types @@ -398,6 +402,55 @@ feature -- Access: url site_url: IMMUTABLE_STRING_8 -- Site url +feature -- Access: WebAPI + + is_webapi_request (req: WSF_REQUEST): BOOLEAN + do + Result := setup.webapi_enabled and then req.percent_encoded_path_info.starts_with_general (setup.webapi_base_path) + end + + webapi_path (a_relative_path: detachable READABLE_STRING_8): STRING_8 + require + is_webapi_enabled: setup.webapi_enabled + do + create Result.make_from_string (setup.webapi_base_path) + if a_relative_path /= Void then + if a_relative_path.is_empty then + Result.append_character ('/') + else + if a_relative_path[1] /= '/' then + Result.append_character ('/') + end + Result.append (a_relative_path) + end + end + end + + webapi_path_location (a_relative_location: detachable READABLE_STRING_8): STRING_8 + require + is_webapi_enabled: setup.webapi_enabled + no_first_slash: a_relative_location = Void or else not a_relative_location.starts_with_general ("/") + do + create Result.make_from_string (webapi_base_path_location) + if a_relative_location /= Void then + if a_relative_location.is_empty then + Result.append_character ('/') + else + if a_relative_location[1] /= '/' then + Result.append_character ('/') + end + Result.append (a_relative_location) + end + end + end + +feature {NONE} -- Implementation/WebAPI. + + webapi_base_path_location: IMMUTABLE_STRING_8 + -- Webapi path without first slash! + +feature -- Access: Administration + is_administration_request (req: WSF_REQUEST): BOOLEAN do Result := req.percent_encoded_path_info.starts_with_general (setup.administration_base_path) @@ -435,13 +488,20 @@ feature -- Access: url end end -feature {NONE} -- Url implementation. +feature {NONE} -- Implementation/ Administration administration_base_path_location: IMMUTABLE_STRING_8 -- Administration path without first slash! feature -- CMS links + webapi_link (a_title: READABLE_STRING_GENERAL; a_relative_location: detachable READABLE_STRING_8): CMS_LOCAL_LINK + require + no_first_slash: a_relative_location = Void or else not a_relative_location.starts_with_general ("/") + do + Result := local_link (a_title, webapi_path_location (a_relative_location)) + end + administration_link (a_title: READABLE_STRING_GENERAL; a_relative_location: detachable READABLE_STRING_8): CMS_LOCAL_LINK require no_first_slash: a_relative_location = Void or else not a_relative_location.starts_with_general ("/") @@ -510,9 +570,17 @@ feature -- Settings switch_to_site_mode do - if is_administration_mode then + if not is_site_mode then setup.set_site_mode - is_administration_mode := False + mode := mode_site + end + end + + switch_to_webapi_mode + do + if not is_webapi_mode then + setup.set_webapi_mode + mode := mode_webapi end end @@ -520,12 +588,32 @@ feature -- Settings do if not is_administration_mode then setup.set_administration_mode - is_administration_mode := True + mode := mode_administration end end + mode: NATURAL_8 + + mode_site: NATURAL_8 = 0 + mode_administration: NATURAL_8 = 1 + mode_webapi: NATURAL_8 = 2 + + is_site_mode: BOOLEAN + do + Result := mode = mode_site + end + is_administration_mode: BOOLEAN -- Is administration mode? + do + Result := mode = mode_administration + end + + is_webapi_mode: BOOLEAN + -- Is webapi mode? + do + Result := mode = mode_webapi + end is_debug: BOOLEAN -- Is debug mode enabled? @@ -969,6 +1057,12 @@ feature {CMS_EXECUTION} -- Hooks else l_module := Void end + elseif is_webapi_mode then + if attached {CMS_WITH_WEBAPI} l_module as wapi then + l_module := wapi.module_webapi + else + l_module := Void + end end if l_module /= Void then if attached {CMS_HOOK_AUTO_REGISTER} l_module as l_auto then @@ -1205,7 +1299,7 @@ feature -- Theming path helpers Result := theme_location_for (a_theme_name).extended ("assets") end -feature -- Environment/ module +feature -- Environment/ module module_configuration (a_module: CMS_MODULE; a_name: detachable READABLE_STRING_GENERAL): detachable CONFIG_READER do @@ -1215,20 +1309,42 @@ feature -- Environment/ module module_configuration_by_name (a_module_name: READABLE_STRING_GENERAL; a_name: detachable READABLE_STRING_GENERAL): detachable CONFIG_READER -- Configuration reader for `a_module', and if `a_name' is set, using name `a_name'. local + k: STRING_32 p: detachable PATH + l_cache: like module_configuration_cache do -- Search first in site/config/modules/$module_name/($app|$module_name).(json|ini) -- if none, look as sub configuration if $app /= Void -- and then in site/modules/$module_name/config/($app|$module_name).(json|ini) -- and if non in sub config if $app /= Void - p := site_location.extended ("config").extended ("modules").extended (a_module_name) - Result := module_configuration_by_name_in_location (a_module_name, p, a_name) - if Result = Void then - p := module_location_by_name (a_module_name).extended ("config") + create k.make_from_string_general (a_module_name) + if a_name /= Void then + k.append_character (':') + k.append_string_general (a_name) + end + l_cache := module_configuration_cache + if l_cache /= Void then + l_cache.search (k) + else + create l_cache.make_caseless (1) + module_configuration_cache := l_cache + end + if l_cache.found then + Result := l_cache.found_item + else + p := site_location.extended ("config").extended ("modules").extended (a_module_name) Result := module_configuration_by_name_in_location (a_module_name, p, a_name) + if Result = Void then + p := module_location_by_name (a_module_name).extended ("config") + Result := module_configuration_by_name_in_location (a_module_name, p, a_name) + end + l_cache.force (Result, k) end end + module_configuration_cache: detachable STRING_TABLE [detachable CONFIG_READER] + -- Cache for `module_configuration(_by_name)` function. + module_configuration_by_name_in_location (a_module_name: READABLE_STRING_GENERAL; a_dir: PATH; a_name: detachable READABLE_STRING_GENERAL): detachable CONFIG_READER -- Configuration reader from "$a_dir/($a_module_name|$a_name).(json|ini)" location. local diff --git a/src/service/cms_execution.e b/src/service/cms_execution.e index 0173c5a..f6ad529 100644 --- a/src/service/cms_execution.e +++ b/src/service/cms_execution.e @@ -19,6 +19,7 @@ inherit execute_default, filter_execute, initialize, + initialize_filter, initialize_router end @@ -61,6 +62,13 @@ feature {NONE} -- Initialization create {WSF_MAINTENANCE_FILTER} filter end + initialize_filter + -- Initialize `filter`. + do + create_filter + -- setup_filter: delayed to `initialize_execution`. + end + initialize_router -- Initialize `router`. do @@ -147,20 +155,34 @@ feature -- Settings: router configure_api_file_handler (l_router) end - setup_router_for_administration - -- + setup_router_and_filter_for_webapi local l_api: like api l_router: like router l_module: CMS_MODULE + f, l_filter, l_last_filter: WSF_FILTER do l_api := api l_router := router - l_api.logger.put_debug (generator + ".setup_router_for_administration", Void) + l_api.logger.put_debug (generator + ".setup_router_for_webapi", Void) -- Configure root of api handler. - l_router.set_base_url (l_api.administration_path (Void)) + l_router.set_base_url (l_api.webapi_path (Void)) + + -- Find insertion location for new filter + -- i.e just before the CMS_EXECUTION filter. + from + l_filter := filter + until + l_last_filter /= Void or not attached l_filter.next as l_next_filter + loop + if l_next_filter.next = Void then + l_last_filter := l_next_filter + else + l_filter := l_next_filter + end + end -- Include routes from modules. across @@ -169,10 +191,86 @@ feature -- Settings: router l_module := ic.item if l_module.is_initialized and then - attached {CMS_ADMINISTRABLE} l_module as l_administration and then - attached l_administration.module_administration as adm + attached {CMS_WITH_WEBAPI} l_module as l_webapi and then + attached l_webapi.module_webapi as wapi then - adm.setup_router (l_router, l_api) + wapi.setup_router (l_router, l_api) + if attached wapi.filters (l_api) as l_filters then + across + l_filters as f_ic + loop + f := f_ic.item + l_filter.set_next (f) + f.set_next (l_last_filter) + l_filter := f + end + end + end + end + end + + setup_router_and_filter_for_administration + -- + local + l_api: like api + l_router: like router + l_module: CMS_MODULE + f, l_filter, l_last_filter: WSF_FILTER + do + l_api := api + l_router := router + + l_api.logger.put_debug (generator + ".setup_router_and_filter_for_administration", Void) + + -- Configure root of api handler. + l_router.set_base_url (l_api.administration_path (Void)) + + -- Apply normal filters + setup_filter + + -- Find insertion location for new filter + -- i.e just before the CMS_EXECUTION filter. + from + l_filter := filter + until + l_last_filter /= Void or not attached l_filter.next as l_next_filter + loop + if l_next_filter.next = Void then + l_last_filter := l_next_filter + else + l_filter := l_next_filter + end + end + + -- Include routes from modules. + across + modules as ic + loop + l_module := ic.item + if + l_module.is_initialized + then + if + attached {CMS_WITH_MODULE_ADMINISTRATION} l_module as l_administration and then + attached l_administration.module_administration as adm + then + adm.setup_router (l_router, l_api) + if attached adm.filters (l_api) as l_filters then + across + l_filters as f_ic + loop + f := f_ic.item + l_filter.set_next (f) + f.set_next (l_last_filter) + l_filter := f + end + end +-- elseif +-- attached {CMS_WITH_WEBAPI} l_module as l_wapi and then +-- attached l_wapi.module_webapi as wapi +-- then +-- wapi.setup_router (l_router, l_api) + end end end map_uri ("/install", create {CMS_ADMIN_INSTALL_HANDLER}.make (api), l_router.methods_head_get) @@ -234,6 +332,8 @@ feature -- Request execution request.set_uploaded_file_path (api.temp_location) if api.is_administration_request (request) then initialize_administration_execution + elseif api.is_webapi_request (request) then + initialize_webapi_execution else initialize_site_execution end @@ -244,15 +344,24 @@ feature -- Request execution do api.switch_to_site_mode api.initialize_execution + setup_filter setup_router end + initialize_webapi_execution + -- Initialize for site execution. + do + api.switch_to_webapi_mode + api.initialize_execution + setup_router_and_filter_for_webapi + end + initialize_administration_execution -- Initialize for administration execution. do api.switch_to_administration_mode api.initialize_execution - setup_router_for_administration + setup_router_and_filter_for_administration end execute @@ -277,7 +386,6 @@ feature -- Filters -- Create `filter'. local f, l_filter: detachable WSF_FILTER - l_module: CMS_MODULE l_api: like api do l_api := api @@ -294,6 +402,32 @@ feature -- Filters -- f.set_next (l_filter) -- l_filter := f + filter := l_filter + end + + setup_filter + -- Setup `filter'. + local + f, l_filter, l_last_filter: detachable WSF_FILTER + l_module: CMS_MODULE + l_api: like api + do + l_api := api + + -- Find insertion location for new filter + -- i.e just before the CMS_EXECUTION filter. + from + l_filter := filter + until + l_last_filter /= Void or not attached l_filter.next as l_next_filter + loop + if l_next_filter.next = Void then + l_last_filter := l_next_filter + else + l_filter := l_next_filter + end + end + -- Include filters from modules across modules as ic @@ -305,18 +439,14 @@ feature -- Filters then across l_m_filters as f_ic loop f := f_ic.item - f.set_next (l_filter) + l_filter.set_next (f) + f.set_next (l_last_filter) +-- f.set_next (l_filter) l_filter := f end end end - - filter := l_filter - end - - setup_filter - -- Setup `filter'. - do +-- filter := l_filter end feature -- Execution diff --git a/src/service/cms_module.e b/src/service/cms_module.e index 7567084..f0ce2e9 100644 --- a/src/service/cms_module.e +++ b/src/service/cms_module.e @@ -80,7 +80,7 @@ feature -- Status is_initialized: BOOLEAN -- Is Current module initialized? -feature {CMS_API, CMS_MODULE_ADMINISTRATION} -- Access: API +feature {CMS_API, CMS_MODULE} -- Access: API module_api: detachable CMS_MODULE_API -- Eventual module api. diff --git a/src/service/cms_module_administration.e b/src/service/cms_module_administration.e index b4c6f4c..50600ef 100644 --- a/src/service/cms_module_administration.e +++ b/src/service/cms_module_administration.e @@ -14,6 +14,8 @@ inherit rename is_initialized as module_is_initialized, is_enabled as module_is_enabled + redefine + module_api end feature {NONE} -- Initialization @@ -53,6 +55,14 @@ feature -- Status Result := module.is_enabled end +feature {CMS_API, CMS_MODULE_ADMINISTRATION, CMS_MODULE_WEBAPI} -- Access: API + + module_api: detachable CMS_MODULE_API + -- Eventual module api. + do + Result := module.module_api + end + feature -- Router setup_router (a_router: WSF_ROUTER; a_api: CMS_API) diff --git a/src/service/cms_with_module_administration.e b/src/service/cms_with_module_administration.e new file mode 100644 index 0000000..cdbdb35 --- /dev/null +++ b/src/service/cms_with_module_administration.e @@ -0,0 +1,36 @@ +note + description: "Interface providing administration module." + date: "$Date$" + revision: "$Revision$" + +deferred class + CMS_WITH_MODULE_ADMINISTRATION + +feature -- Administration + + module_administration: like administration + -- Associated administration module. + do + Result := internal_module_administration + if Result = Void then + Result := administration + internal_module_administration := Result + end + end + +feature {NONE} -- Implementation + + internal_module_administration: detachable like module_administration + -- Cached version of `module_administration`. + +feature {NONE} -- Administration + + administration: CMS_MODULE_ADMINISTRATION [CMS_MODULE] + -- Administration module. + deferred + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/modules/auth/cms_auth_filter_i.e b/src/service/filter/cms_auth_filter.e similarity index 57% rename from modules/auth/cms_auth_filter_i.e rename to src/service/filter/cms_auth_filter.e index 21f566e..fe69c77 100644 --- a/modules/auth/cms_auth_filter_i.e +++ b/src/service/filter/cms_auth_filter.e @@ -6,10 +6,13 @@ note revision: "$Revision$" deferred class - CMS_AUTH_FILTER_I + CMS_AUTH_FILTER inherit WSF_FILTER + rename + execute as auth_execute + end feature {NONE} -- Initialization @@ -25,20 +28,28 @@ feature -- API Service feature -- Basic operations - execute (req: WSF_REQUEST; res: WSF_RESPONSE) + auth_execute (req: WSF_REQUEST; res: WSF_RESPONSE) -- - deferred + do + -- If user is already authenticated, do not try current authenticating filter + -- and go to next filter directly. + if api.user_is_authenticated then + execute_next (req, res) + else + execute (req, res) + end end - auth_strategy: STRING + execute (req: WSF_REQUEST; res: WSF_RESPONSE) + -- + require + no_user_authenticated: api.user = Void deferred end set_current_user (u: CMS_USER) do api.set_user (u) - -- Record auth strategy: - api.set_execution_variable ("auth_strategy", auth_strategy) end end diff --git a/src/service/response/cms_response.e b/src/service/response/cms_response.e index 79aac8b..0bf9b78 100644 --- a/src/service/response/cms_response.e +++ b/src/service/response/cms_response.e @@ -1,6 +1,6 @@ note description: "[ - Generic CMS Response. + Generic CMS Web Response. It builds the content to get process to render the output. ]" date: "$Date$" @@ -10,64 +10,33 @@ deferred class CMS_RESPONSE inherit - CMS_URL_UTILITIES + CMS_RESPONSE_I + redefine + make, initialize + end - REFACTORING_HELPER + CMS_URL_UTILITIES feature {NONE} -- Initialization make (req: WSF_REQUEST; res: WSF_RESPONSE; a_api: like api) do - status_code := {HTTP_STATUS_CODE}.ok - api := a_api - request := req - response := res - create header.make create values.make (3) - site_url := a_api.site_url - if attached a_api.base_url as l_base_url then - base_url := l_base_url - end - base_path := a_api.base_path - initialize + Precursor (req, res, a_api) end initialize - local - s: READABLE_STRING_8 do + Precursor get_theme create menu_system.make initialize_block_region_settings - - s := request.percent_encoded_path_info - if not s.is_empty and then s[1] = '/' then - create location.make_from_string (s.substring (2, s.count)) - else - create location.make_from_string (s) - end end feature -- Access - request: WSF_REQUEST - - response: WSF_RESPONSE - - status_code: INTEGER - - header: WSF_HEADER - main_content: detachable STRING_8 -feature -- Settings - - is_administration_mode: BOOLEAN - -- Is administration mode? - do - Result := api.is_administration_mode - end - feature -- Access: metadata title: detachable READABLE_STRING_32 @@ -77,96 +46,19 @@ feature -- Access: metadata description: detachable READABLE_STRING_32 - keywords: detachable READABLE_STRING_32 - - publication_date: detachable DATE_TIME - -- Optional publication date. - - modification_date: detachable DATE_TIME - -- Optional modification date. - additional_page_head_lines: detachable LIST [READABLE_STRING_8] -- HTML>head>...extra lines - redirection: detachable READABLE_STRING_8 - -- Location for eventual redirection. - - redirection_delay: NATURAL - -- Optional redirection delay in seconds. - -feature -- Access: query - - location: IMMUTABLE_STRING_8 - -- Associated cms local location. - - request_url (opts: detachable CMS_API_OPTIONS): STRING_8 - -- Current request location as a url. - do - Result := url (location, opts) - end - feature -- API - api: CMS_API - -- Current CMS API. - - setup: CMS_SETUP - -- Current setup - do - Result := api.setup - end - formats: CMS_FORMATS -- Available content formats. do Result := api.formats end -feature -- URL utilities - - is_front: BOOLEAN - -- Is current response related to "front" page? - local - l_path_info: READABLE_STRING_8 - do - l_path_info := request.percent_encoded_path_info - if attached setup.front_page_path as l_front_page_path then - Result := l_front_page_path.same_string (l_path_info) - else - if base_path.same_string (l_path_info) then - Result := True - else - Result := l_path_info.is_empty or else l_path_info.same_string ("/") - end - end - end - - site_url: IMMUTABLE_STRING_8 - -- Absolute site url. - -- Always ends with '/' - - base_url: detachable IMMUTABLE_STRING_8 - -- Base url if any. - --| Usually it is Void, but it could be - --| /project/demo/ - - base_path: IMMUTABLE_STRING_8 - -- Base path, default to "/". - -- Always ends with '/' - -- Could be /project/demo/ - feature -- Access: CMS - site_name: STRING_32 - do - Result := setup.site_name - end - - front_page_url: READABLE_STRING_8 - do - Result := absolute_url ("/", Void) - end - values: CMS_VALUE_TABLE -- Associated values indexed by string name. @@ -177,72 +69,6 @@ feature -- Specific values Result := values.item ("optional_content_type") end -feature -- User access - - is_authenticated: BOOLEAN - -- Is user authenticated? - do - Result := user /= Void - end - - user: detachable CMS_USER - -- Active user if authenticated. - do - Result := api.user - end - - set_user (u: CMS_USER) - -- Set active user to `u'. - require - attached_u: u /= Void - do - api.set_user (u) - end - - unset_user - -- Unset active user. - do - api.unset_user - end - -feature -- Permission - - has_permission_on_link (a_link: CMS_LINK): BOOLEAN - -- Does current user has permission to access link `a_link'? - do - Result := True - if - attached {CMS_LOCAL_LINK} a_link as lnk and then - attached lnk.permission_arguments as l_perms - then - Result := has_permissions (l_perms) - end - end - - has_permission (a_permission: READABLE_STRING_GENERAL): BOOLEAN - -- Does current user has permission `a_permission' ? - do - Result := user_has_permission (user, a_permission) - end - - has_permissions (a_permission_list: ITERABLE [READABLE_STRING_GENERAL]): BOOLEAN - -- Does current user has any of the permissions `a_permission_list' ? - do - Result := user_has_permissions (user, a_permission_list) - end - - user_has_permission (a_user: detachable CMS_USER; a_permission: READABLE_STRING_GENERAL): BOOLEAN - -- Does `a_user' has permission `a_permission' ? - do - Result := api.user_has_permission (a_user, a_permission) - end - - user_has_permissions (a_user: detachable CMS_USER; a_permission_list: ITERABLE [READABLE_STRING_GENERAL]): BOOLEAN - -- Does `a_user' has any of the permissions `a_permission_list' ? - do - Result := api.user_has_permissions (a_user, a_permission_list) - end - feature -- Head customization add_additional_head_line (s: READABLE_STRING_8; a_allow_duplication: BOOLEAN) @@ -322,27 +148,6 @@ feature -- Element change description := d end - set_keywords (s: like keywords) - do - keywords := s - end - - set_publication_date (dt: like publication_date) - do - publication_date := dt - if dt /= Void and modification_date = Void then - modification_date := dt - end - end - - set_modification_date (dt: like modification_date) - do - modification_date := dt - if dt /= Void and publication_date = Void then - publication_date := dt - end - end - set_main_content (s: like main_content) do main_content := s @@ -365,34 +170,6 @@ feature -- Element change values.remove (k) end - set_redirection (a_location: READABLE_STRING_8) - -- Set `redirection' to `a_location'. - do - redirection := a_location - end - - set_redirection_delay (nb_secs: NATURAL) - do - redirection_delay := nb_secs - end - -feature -- Logging - - log (a_category: READABLE_STRING_8; a_message: READABLE_STRING_8; a_level: INTEGER; a_link: detachable CMS_LINK) - local --- l_log: CMS_LOG - do - debug - to_implement ("Add implementation") - end --- create l_log.make (a_category, a_message, a_level, Void) --- if a_link /= Void then --- l_log.set_link (a_link) --- end --- l_log.set_info (request.http_user_agent) --- service.storage.save_log (l_log) - end - feature -- Menu menu_system: CMS_MENU_SYSTEM @@ -904,16 +681,6 @@ feature -- Blocks Result.set_is_raw (True) end -feature -- Hooks - - hooks: CMS_HOOK_CORE_MANAGER - -- Manager handling hook subscriptions. - obsolete - "Use api.hooks [2017-05-31]" - do - Result := api.hooks - end - feature -- Menu: change add_to_main_menu (lnk: CMS_LINK) @@ -938,22 +705,6 @@ feature -- Menu: change m.extend (lnk) end -feature -- Internationalization (i18n) - - translation (a_text: READABLE_STRING_GENERAL; opts: detachable CMS_API_OPTIONS): STRING_32 - -- Translated text `a_text' according to expected context (lang, ...) - -- and adapt according to options eventually set by `opts'. - do - Result := api.translation (a_text, opts) - end - - formatted_string (a_text: READABLE_STRING_GENERAL; args: TUPLE): STRING_32 - -- Format `a_text' using arguments `args'. - --| ex: formatted_string ("hello $1, see page $title.", ["bob", "contact"] -> "hello bob, see page contact" - do - Result := api.formatted_string (a_text, args) - end - feature -- Message add_message (a_msg: READABLE_STRING_8; a_category: detachable READABLE_STRING_8) @@ -1068,19 +819,6 @@ feature {NONE} -- Theme helpers internal_wsf_theme: detachable WSF_THEME -- Once per object for `wsf_theme'. -feature -- Element Change - - set_status_code (a_status: INTEGER) - -- Set `status_code' with `a_status'. - note - EIS: "src=eiffel:?class=HTTP_STATUS_CODE" - do - to_implement ("Feature to test if a_status is a valid status code!!!.") - status_code := a_status - ensure - status_code_set: status_code = a_status - end - feature -- Cache managment clear_cache (a_cache_id_list: detachable ITERABLE [READABLE_STRING_GENERAL]) @@ -1366,32 +1104,8 @@ feature -- Generation a_lnk.set_is_forbidden (not has_permission_on_link (a_lnk)) end -feature -- Helpers: cms link - - administration_link (a_title: READABLE_STRING_GENERAL; a_relative_location: detachable READABLE_STRING_8): CMS_LOCAL_LINK - require - no_first_slash: a_relative_location = Void or else not a_relative_location.starts_with_general ("/") - do - Result := api.administration_link (a_title, a_relative_location) - end - - local_link (a_title: READABLE_STRING_GENERAL; a_location: READABLE_STRING_8): CMS_LOCAL_LINK - do - Result := api.local_link (a_title, a_location) - end - - user_local_link (u: CMS_USER; a_opt_title: detachable READABLE_STRING_GENERAL): CMS_LOCAL_LINK - do - Result := api.user_local_link (u, a_opt_title) - end - feature -- Helpers: html links - user_profile_name, user_display_name (u: CMS_USER): READABLE_STRING_32 - do - Result := api.user_display_name (u) - end - user_html_link (u: CMS_USER): STRING require u_with_name: not u.name.is_whitespace @@ -1399,43 +1113,6 @@ feature -- Helpers: html links Result := api.user_html_link (u) end -feature -- Helpers: URLs - - location_absolute_url (a_location: READABLE_STRING_8; opts: detachable CMS_API_OPTIONS): STRING - -- Absolute URL for `a_location'. - --| Options `opts' could be - --| - absolute: True|False => return absolute url - --| - query: string => append "?query" - --| - fragment: string => append "#fragment" - do - Result := api.location_absolute_url (a_location, opts) - end - - location_url (a_location: READABLE_STRING_8; opts: detachable CMS_API_OPTIONS): STRING - -- URL for `a_location'. - --| Options `opts' could be - --| - absolute: True|False => return absolute url - --| - query: string => append "?query" - --| - fragment: string => append "#fragment" - do - Result := api.location_url (a_location, opts) - end - - module_resource_url (a_module: CMS_MODULE; a_path: READABLE_STRING_8; opts: detachable CMS_API_OPTIONS): STRING_8 - -- Url for resource `a_path` associated with module `a_module`. - require - a_valid_valid: a_path.is_empty or else a_path.starts_with ("/") - do - Result := url ("/module/" + a_module.name + a_path, opts) - end - - user_url (u: CMS_USER): like url - require - u_with_id: u.has_id - do - Result := api.user_url (u) - end - feature -- Execution execute @@ -1499,7 +1176,6 @@ feature {NONE} -- Execution on_terminated do - end note diff --git a/src/service/response/cms_response_i.e b/src/service/response/cms_response_i.e new file mode 100644 index 0000000..767ed84 --- /dev/null +++ b/src/service/response/cms_response_i.e @@ -0,0 +1,376 @@ +note + description: "[ + Generic CMS Response. + It builds the content to get process to render the output. + ]" + date: "$Date$" + revision: "$Revision$" + +deferred class + CMS_RESPONSE_I + +inherit + CMS_URL_UTILITIES + + REFACTORING_HELPER + +feature {NONE} -- Initialization + + make (req: WSF_REQUEST; res: WSF_RESPONSE; a_api: like api) + do + status_code := {HTTP_STATUS_CODE}.ok + api := a_api + request := req + response := res + create header.make + site_url := a_api.site_url + if attached a_api.base_url as l_base_url then + base_url := l_base_url + end + base_path := a_api.base_path + initialize + end + + initialize + local + s: READABLE_STRING_8 + do + s := request.percent_encoded_path_info + if not s.is_empty and then s[1] = '/' then + create location.make_from_string (s.substring (2, s.count)) + else + create location.make_from_string (s) + end + end + +feature -- Access + + request: WSF_REQUEST + + response: WSF_RESPONSE + + status_code: INTEGER + + header: WSF_HEADER + +feature -- Settings + + is_site_mode: BOOLEAN + -- Is site mode? + do + Result := api.is_site_mode + end + + is_webapi_mode: BOOLEAN + -- Is Web API mode? + do + Result := api.is_webapi_mode + end + + is_administration_mode: BOOLEAN + -- Is administration mode? + do + Result := api.is_administration_mode + end + +feature -- Access: metadata + + keywords: detachable READABLE_STRING_32 + + publication_date: detachable DATE_TIME + -- Optional publication date. + + modification_date: detachable DATE_TIME + -- Optional modification date. + + redirection: detachable READABLE_STRING_8 + -- Location for eventual redirection. + + redirection_delay: NATURAL + -- Optional redirection delay in seconds. + +feature -- Access: query + + location: IMMUTABLE_STRING_8 + -- Associated cms local location. + + request_url (opts: detachable CMS_API_OPTIONS): STRING_8 + -- Current request location as a url. + do + Result := url (location, opts) + end + +feature -- API + + api: CMS_API + -- Current CMS API. + + setup: CMS_SETUP + -- Current setup + do + Result := api.setup + end + +feature -- URL utilities + + is_front: BOOLEAN + -- Is current response related to "front" page? + local + l_path_info: READABLE_STRING_8 + do + l_path_info := request.percent_encoded_path_info + if attached setup.front_page_path as l_front_page_path then + Result := l_front_page_path.same_string (l_path_info) + else + if base_path.same_string (l_path_info) then + Result := True + else + Result := l_path_info.is_empty or else l_path_info.same_string ("/") + end + end + end + + site_url: IMMUTABLE_STRING_8 + -- Absolute site url. + -- Always ends with '/' + + base_url: detachable IMMUTABLE_STRING_8 + -- Base url if any. + --| Usually it is Void, but it could be + --| /project/demo/ + + base_path: IMMUTABLE_STRING_8 + -- Base path, default to "/". + -- Always ends with '/' + -- Could be /project/demo/ + +feature -- Access: CMS + + site_name: STRING_32 + do + Result := setup.site_name + end + + front_page_url: READABLE_STRING_8 + do + Result := absolute_url ("/", Void) + end + +feature -- User access + + is_authenticated: BOOLEAN + -- Is user authenticated? + do + Result := user /= Void + end + + user: detachable CMS_USER + -- Active user if authenticated. + do + Result := api.user + end + + set_user (u: CMS_USER) + -- Set active user to `u'. + require + attached_u: u /= Void + do + api.set_user (u) + end + + unset_user + -- Unset active user. + do + api.unset_user + end + +feature -- Permission + + has_permission_on_link (a_link: CMS_LINK): BOOLEAN + -- Does current user has permission to access link `a_link'? + do + Result := True + if + attached {CMS_LOCAL_LINK} a_link as lnk and then + attached lnk.permission_arguments as l_perms + then + Result := has_permissions (l_perms) + end + end + + has_permission (a_permission: READABLE_STRING_GENERAL): BOOLEAN + -- Does current user has permission `a_permission' ? + do + Result := user_has_permission (user, a_permission) + end + + has_permissions (a_permission_list: ITERABLE [READABLE_STRING_GENERAL]): BOOLEAN + -- Does current user has any of the permissions `a_permission_list' ? + do + Result := user_has_permissions (user, a_permission_list) + end + + user_has_permission (a_user: detachable CMS_USER; a_permission: READABLE_STRING_GENERAL): BOOLEAN + -- Does `a_user' has permission `a_permission' ? + do + Result := api.user_has_permission (a_user, a_permission) + end + + user_has_permissions (a_user: detachable CMS_USER; a_permission_list: ITERABLE [READABLE_STRING_GENERAL]): BOOLEAN + -- Does `a_user' has any of the permissions `a_permission_list' ? + do + Result := api.user_has_permissions (a_user, a_permission_list) + end + +feature -- Element change + + set_keywords (s: like keywords) + do + keywords := s + end + + set_publication_date (dt: like publication_date) + do + publication_date := dt + if dt /= Void and modification_date = Void then + modification_date := dt + end + end + + set_modification_date (dt: like modification_date) + do + modification_date := dt + if dt /= Void and publication_date = Void then + publication_date := dt + end + end + + set_redirection (a_location: READABLE_STRING_8) + -- Set `redirection' to `a_location'. + do + redirection := a_location + end + + set_redirection_delay (nb_secs: NATURAL) + do + redirection_delay := nb_secs + end + +feature -- Hooks + + hooks: CMS_HOOK_CORE_MANAGER + -- Manager handling hook subscriptions. + obsolete + "Use api.hooks [2017-05-31]" + do + Result := api.hooks + end + +feature -- Internationalization (i18n) + + translation (a_text: READABLE_STRING_GENERAL; opts: detachable CMS_API_OPTIONS): STRING_32 + -- Translated text `a_text' according to expected context (lang, ...) + -- and adapt according to options eventually set by `opts'. + do + Result := api.translation (a_text, opts) + end + + formatted_string (a_text: READABLE_STRING_GENERAL; args: TUPLE): STRING_32 + -- Format `a_text' using arguments `args'. + --| ex: formatted_string ("hello $1, see page $title.", ["bob", "contact"] -> "hello bob, see page contact" + do + Result := api.formatted_string (a_text, args) + end + +feature -- Element Change + + set_status_code (a_status: INTEGER) + -- Set `status_code' with `a_status'. + note + EIS: "src=eiffel:?class=HTTP_STATUS_CODE" + do + status_code := a_status + ensure + status_code_set: status_code = a_status + end + +feature -- Helpers: cms link + + webapi_link (a_title: READABLE_STRING_GENERAL; a_relative_location: detachable READABLE_STRING_8): CMS_LOCAL_LINK + require + no_first_slash: a_relative_location = Void or else not a_relative_location.starts_with_general ("/") + do + Result := api.webapi_link (a_title, a_relative_location) + end + + administration_link (a_title: READABLE_STRING_GENERAL; a_relative_location: detachable READABLE_STRING_8): CMS_LOCAL_LINK + require + no_first_slash: a_relative_location = Void or else not a_relative_location.starts_with_general ("/") + do + Result := api.administration_link (a_title, a_relative_location) + end + + local_link (a_title: READABLE_STRING_GENERAL; a_location: READABLE_STRING_8): CMS_LOCAL_LINK + do + Result := api.local_link (a_title, a_location) + end + + user_local_link (u: CMS_USER; a_opt_title: detachable READABLE_STRING_GENERAL): CMS_LOCAL_LINK + do + Result := api.user_local_link (u, a_opt_title) + end + +feature -- Helpers: html links + + user_profile_name, user_display_name (u: CMS_USER): READABLE_STRING_32 + do + Result := api.user_display_name (u) + end + +feature -- Helpers: URLs + + location_absolute_url (a_location: READABLE_STRING_8; opts: detachable CMS_API_OPTIONS): STRING + -- Absolute URL for `a_location'. + --| Options `opts' could be + --| - absolute: True|False => return absolute url + --| - query: string => append "?query" + --| - fragment: string => append "#fragment" + do + Result := api.location_absolute_url (a_location, opts) + end + + location_url (a_location: READABLE_STRING_8; opts: detachable CMS_API_OPTIONS): STRING + -- URL for `a_location'. + --| Options `opts' could be + --| - absolute: True|False => return absolute url + --| - query: string => append "?query" + --| - fragment: string => append "#fragment" + do + Result := api.location_url (a_location, opts) + end + + module_resource_url (a_module: CMS_MODULE; a_path: READABLE_STRING_8; opts: detachable CMS_API_OPTIONS): STRING_8 + -- Url for resource `a_path` associated with module `a_module`. + require + a_valid_valid: a_path.is_empty or else a_path.starts_with ("/") + do + Result := url ("/module/" + a_module.name + a_path, opts) + end + + user_url (u: CMS_USER): like url + require + u_with_id: u.has_id + do + Result := api.user_url (u) + end + +feature -- Execution + + execute + deferred + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/service/response/error/bad_request_error_cms_response.e b/src/service/response/error/bad_request_error_cms_response.e index 747cde4..b9abb6b 100644 --- a/src/service/response/error/bad_request_error_cms_response.e +++ b/src/service/response/error/bad_request_error_cms_response.e @@ -7,7 +7,6 @@ class BAD_REQUEST_ERROR_CMS_RESPONSE inherit - CMS_RESPONSE redefine custom_prepare @@ -37,6 +36,6 @@ feature -- Execution end note - copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/src/service/response/error/forbidden_error_cms_response.e b/src/service/response/error/forbidden_error_cms_response.e index 7b74803..4738a1f 100644 --- a/src/service/response/error/forbidden_error_cms_response.e +++ b/src/service/response/error/forbidden_error_cms_response.e @@ -7,7 +7,6 @@ class FORBIDDEN_ERROR_CMS_RESPONSE inherit - CMS_RESPONSE redefine custom_prepare diff --git a/src/service/response/error/internal_server_error_cms_response.e b/src/service/response/error/internal_server_error_cms_response.e index cd3aa69..ffb11a1 100644 --- a/src/service/response/error/internal_server_error_cms_response.e +++ b/src/service/response/error/internal_server_error_cms_response.e @@ -7,7 +7,6 @@ class INTERNAL_SERVER_ERROR_CMS_RESPONSE inherit - CMS_RESPONSE redefine custom_prepare @@ -36,7 +35,7 @@ feature -- Execution set_main_content ("Internal Server Error") end note - copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/src/service/response/error/not_found_error_cms_response.e b/src/service/response/error/not_found_error_cms_response.e index 40b6c8e..4efd0a5 100644 --- a/src/service/response/error/not_found_error_cms_response.e +++ b/src/service/response/error/not_found_error_cms_response.e @@ -7,7 +7,6 @@ class NOT_FOUND_ERROR_CMS_RESPONSE inherit - CMS_RESPONSE redefine custom_prepare @@ -36,7 +35,7 @@ feature -- Execution set_main_content ("The requested page %"" + request.request_uri + "%"could not be found.") end note - copyright: "2011-2016, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/src/service/response/error/not_implemented_error_cms_response.e b/src/service/response/error/not_implemented_error_cms_response.e index 72f2a21..8a0ff11 100644 --- a/src/service/response/error/not_implemented_error_cms_response.e +++ b/src/service/response/error/not_implemented_error_cms_response.e @@ -7,7 +7,6 @@ class NOT_IMPLEMENTED_ERROR_CMS_RESPONSE inherit - CMS_RESPONSE redefine custom_prepare @@ -38,7 +37,7 @@ feature -- Execution end end note - copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/src/service/response/generic_view_cms_response.e b/src/service/response/generic_view_cms_response.e index 4ff76c6..a8e8988 100644 --- a/src/service/response/generic_view_cms_response.e +++ b/src/service/response/generic_view_cms_response.e @@ -19,7 +19,7 @@ feature -- Execution do end note - copyright: "2011-2015, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/src/service/response/hm_webapi_response.e b/src/service/response/hm_webapi_response.e new file mode 100644 index 0000000..aa2f452 --- /dev/null +++ b/src/service/response/hm_webapi_response.e @@ -0,0 +1,51 @@ +note + description: "Summary description for Hyper media {HM_WEBAPI_RESPONSE}." + date: "$Date$" + revision: "$Revision$" + +deferred class + HM_WEBAPI_RESPONSE + +inherit + WEBAPI_RESPONSE + +feature -- Element change + + add_self (a_href: READABLE_STRING_8) + deferred + end + + add_string_field (a_name: READABLE_STRING_GENERAL; a_value: READABLE_STRING_GENERAL) + deferred + end + + add_boolean_field (a_name: READABLE_STRING_GENERAL; a_value: BOOLEAN) + deferred + end + + add_integer_64_field (a_name: READABLE_STRING_GENERAL; a_value: INTEGER_64) + deferred + end + + add_iterator_field (a_name: READABLE_STRING_GENERAL; a_value: ITERABLE [detachable ANY]) + deferred + end + + add_table_iterator_field (a_name: READABLE_STRING_GENERAL; a_value: TABLE_ITERABLE [detachable ANY, READABLE_STRING_GENERAL]) + deferred + end + + add_link (rel: READABLE_STRING_8; a_attname: detachable READABLE_STRING_8; a_att_href: READABLE_STRING_8) + deferred + end + + add_templated_link (rel: READABLE_STRING_8; a_attname: detachable READABLE_STRING_8; a_att_href: READABLE_STRING_8) + deferred + end + +invariant + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/service/response/home_cms_response.e b/src/service/response/home_cms_response.e index 7985506..d2f1ce4 100644 --- a/src/service/response/home_cms_response.e +++ b/src/service/response/home_cms_response.e @@ -7,7 +7,6 @@ class HOME_CMS_RESPONSE inherit - CMS_RESPONSE redefine custom_prepare @@ -32,5 +31,8 @@ feature -- Execution set_title (Void) set_page_title (Void) end +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" end diff --git a/src/service/response/json_webapi_response.e b/src/service/response/json_webapi_response.e new file mode 100644 index 0000000..d2828d6 --- /dev/null +++ b/src/service/response/json_webapi_response.e @@ -0,0 +1,201 @@ +note + description: "Summary description for JSON {JSON_WEBAPI_RESPONSE}." + date: "$Date$" + revision: "$Revision$" + +class + JSON_WEBAPI_RESPONSE + +inherit + HM_WEBAPI_RESPONSE + redefine + initialize + end + +create + make + +feature {NONE} -- Initialization + + initialize + do + Precursor + create resource.make_empty + end + +feature -- Access + + resource: JSON_OBJECT + +feature -- Element change + + add_self (a_href: READABLE_STRING_8) + do + add_link ("self", Void, a_href) + end + +feature -- Fields + +-- add_field (a_name: READABLE_STRING_GENERAL; a_value: detachable ANY) +-- do +-- resource.put (new_resource_item (a_value), a_name) +-- end + + add_string_field (a_name: READABLE_STRING_GENERAL; a_value: READABLE_STRING_GENERAL) + do + resource.put_string (a_value, a_name) + end + + add_boolean_field (a_name: READABLE_STRING_GENERAL; a_value: BOOLEAN) + do + resource.put_boolean (a_value, a_name) + end + + add_integer_64_field (a_name: READABLE_STRING_GENERAL; a_value: INTEGER_64) + do + resource.put_integer (a_value, a_name) + end + + add_natural_64_field (a_name: READABLE_STRING_GENERAL; a_value: NATURAL_64) + do + resource.put_natural (a_value, a_name) + end + + add_real_64_field (a_name: READABLE_STRING_GENERAL; a_value: REAL_64) + do + resource.put_real (a_value, a_name) + end + + add_iterator_field (a_name: READABLE_STRING_GENERAL; a_value: ITERABLE [detachable ANY]) + do + resource.put (new_resource_item (a_value), a_name) + end + + add_table_iterator_field (a_name: READABLE_STRING_GENERAL; a_value: TABLE_ITERABLE [detachable ANY, READABLE_STRING_GENERAL]) + do + resource.put (new_resource_item (a_value), a_name) + end + +feature -- Links + + add_link (rel: READABLE_STRING_8; a_attname: detachable READABLE_STRING_8 ; a_att_href: READABLE_STRING_8) + local + lnks: JSON_OBJECT + lnk: JSON_OBJECT + do + if attached {JSON_OBJECT} resource.item ("_links") as j_links then + lnks := j_links + else + create lnks.make_with_capacity (1) + resource.put (lnks, "_links") + end + create lnk.make_with_capacity (2) + if a_attname /= Void then + lnk.put_string (a_attname, "name") + end + + lnk.put_string (a_att_href, "href") + lnks.put (lnk, rel) + end + + add_templated_link (rel: READABLE_STRING_8; a_attname: detachable READABLE_STRING_8; a_att_href: READABLE_STRING_8) + local + lnks: JSON_OBJECT + lnk: JSON_OBJECT + do + if attached {JSON_OBJECT} resource.item ("_links") as j_links then + lnks := j_links + else + create lnks.make_with_capacity (1) + resource.put (lnks, "_links") + end + create lnk.make_with_capacity (2) + if a_attname /= Void then + lnk.put_string (a_attname, "name") + end + + lnk.put_string (a_att_href, "href") + lnk.put_boolean (True, "templated") + lnks.put (lnk, rel) + end + + +feature -- Execution + + process + local + m: WSF_PAGE_RESPONSE + do + create m.make_with_body (resource.representation) + m.header.put_content_type ("application/json") + response.send (m) + end + +feature {NONE} -- Implementation factory + + new_resource_item (a_value: detachable ANY): JSON_VALUE + local + l_serializer: JSON_REFLECTOR_SERIALIZER + ctx: JSON_SERIALIZER_CONTEXT + do + create {JSON_NULL} Result + + create l_serializer + create ctx + ctx.set_default_serializer (l_serializer) + ctx.set_is_type_name_included (False) + ctx.register_serializer (create {TABLE_ITERABLE_JSON_SERIALIZER [detachable ANY, READABLE_STRING_GENERAL]}, {TABLE_ITERABLE [detachable ANY, READABLE_STRING_GENERAL]}) + ctx.register_serializer (create {ITERABLE_JSON_SERIALIZER [detachable ANY]}, {ITERABLE [detachable ANY]}) + Result := l_serializer.to_json (a_value, ctx) +-- if a_value = Void then +-- create {JSON_NULL} Result +-- elseif attached {READABLE_STRING_GENERAL} a_value as s then +-- create {JSON_STRING} Result.make_from_string_general (s) +-- elseif attached {BOOLEAN} a_value as b then +-- create {JSON_BOOLEAN} Result.make (b) +-- elseif attached {NUMERIC} a_value as num then +---- if attached {INTEGER_64} num as i64 then +---- add_integer_64_field (a_name, i64) +---- elseif attached {INTEGER_32} num as i32 then +---- add_integer_64_field (a_name, i32.as_integer_64) +---- elseif attached {INTEGER_16} num as i16 then +---- add_integer_64_field (a_name, i16.as_integer_64) +---- elseif attached {INTEGER_8} num as i8 then +---- add_integer_64_field (a_name, i8.as_integer_64) +---- elseif attached {NATURAL_64} num as n64 then +---- add_natural_64_field (a_name, n64) +---- elseif attached {NATURAL_32} num as n32 then +---- add_natural_64_field (a_name, n32.as_natural_64) +---- elseif attached {NATURAL_16} num as n16 then +---- add_natural_64_field (a_name, n16.as_natural_64) +---- elseif attached {NATURAL_8} num as n8 then +---- add_natural_64_field (a_name, n8.as_natural_64) +---- elseif attached {REAL_64} num as r64 then +---- add_real_64_field (a_name, r64) +---- elseif attached {REAL_32} num as r32 then +---- add_real_64_field (a_name, r32.to_double +---- else +---- check is_basic_numeric_type: False end +---- add_string_field (a_name, num.out) +---- end +-- elseif attached {CHARACTER_8} a_value as ch8 then +---- add_string_field (a_name, ch8.out) +-- elseif attached {CHARACTER_32} a_value as ch32 then +---- add_string_field (a_name, ch32.out) +-- elseif attached {POINTER} a_value as ptr then +---- add_string_field (a_name, ptr.out) +-- elseif attached {ITERABLE [detachable ANY]} a_value as arr then +---- add_iterator_field (a_name, arr) +-- elseif attached {TABLE_ITERABLE [detachable ANY, READABLE_STRING_GENERAL]} a_value as tb then +---- add_table_iterator_field (a_name, tb) +-- else +-- check is_supported_type: False end +-- end + end + +invariant + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/service/response/md_webapi_response.e b/src/service/response/md_webapi_response.e new file mode 100644 index 0000000..12931d1 --- /dev/null +++ b/src/service/response/md_webapi_response.e @@ -0,0 +1,70 @@ +note + description: "Summary description for Microdata {MD_WEBAPI_RESPONSE}." + date: "$Date$" + revision: "$Revision$" + +class + MD_WEBAPI_RESPONSE + +inherit + HM_WEBAPI_RESPONSE + redefine + initialize + end + +create + make + +feature {NONE} -- Initialization + + initialize + do + Precursor + create resource.make + end + +feature -- Access + + resource: MD_DOCUMENT + +feature -- Element change + + add_self (a_href: READABLE_STRING_8) + do + add_field ("self", a_href) + end + + add_field (a_name: READABLE_STRING_GENERAL; a_value: READABLE_STRING_GENERAL) + local + p: MD_PROPERTY + do + create p.make ("self", a_value, Void) + resource.put (p) + end + + add_link (rel: READABLE_STRING_8; a_attname: READABLE_STRING_8 ; a_att_href: READABLE_STRING_8) + local + i: MD_ITEM + do + -- TODO + end + +feature -- Execution + + execute + local + m: WSF_PAGE_RESPONSE + do + -- TODO + create m.make_with_body ("NOT IMPLEMENTED") + m.set_status_code ({HTTP_STATUS_CODE}.not_implemented) + m.header.put_content_type ("application/xhtml+xml") + response.send (m) + end + +invariant + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/service/response/webapi_response.e b/src/service/response/webapi_response.e new file mode 100644 index 0000000..971fb85 --- /dev/null +++ b/src/service/response/webapi_response.e @@ -0,0 +1,44 @@ +note + description: "[ + Generic Web API Response. + ]" + date: "$Date$" + revision: "$Revision$" + +deferred class + WEBAPI_RESPONSE + +inherit + CMS_RESPONSE_I + +feature -- Status report + + is_root: BOOLEAN + -- Is current response related to root api endpoint? + local + l_path_info: READABLE_STRING_8 + do + l_path_info := request.percent_encoded_path_info + if l_path_info.ends_with_general ("/") then + l_path_info := l_path_info.substring (1, l_path_info.count - 1) + end + Result := l_path_info.same_string (api.setup.webapi_base_path) + end + +feature -- Execution + + execute + do + api.hooks.invoke_webapi_response_alter (Current) + process + end + + process + -- Execute Current webapi response. + deferred + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/service/webapi/cms_module_webapi.e b/src/service/webapi/cms_module_webapi.e new file mode 100644 index 0000000..8e954d1 --- /dev/null +++ b/src/service/webapi/cms_module_webapi.e @@ -0,0 +1,90 @@ +note + description: "[ + Objects that ... + ]" + author: "$Author$" + date: "$Date$" + revision: "$Revision$" + +deferred class + CMS_MODULE_WEBAPI [G -> CMS_MODULE] + +inherit + CMS_MODULE + rename + is_initialized as module_is_initialized, + is_enabled as module_is_enabled + redefine + module_api + end + +feature {NONE} -- Initialization + + make (a_module: G) + -- Initialize `Current'. + do + module := a_module + version := a_module.version + description := a_module.description + package := a_module.package + + module_is_initialized := a_module.is_initialized + module_is_enabled := a_module.is_enabled + end + +feature -- Access + + module: G + + name: STRING + do + Result := module.name + end + +feature {CMS_API, CMS_MODULE_ADMINISTRATION, CMS_MODULE_WEBAPI} -- Access: API + + module_api: detachable CMS_MODULE_API + -- Eventual module api. + do + Result := module.module_api + end + +feature -- Status + + is_initialized: BOOLEAN + -- Is Current module initialized? + do + Result := module.is_initialized + end + + is_enabled: BOOLEAN + -- Is Current module enabled? + do + Result := module.is_enabled + end + +feature -- Router + + setup_router (a_router: WSF_ROUTER; a_api: CMS_API) + -- + do + if a_router.base_url /= Void then + setup_webapi_router (a_router, a_api) + end + end + +feature {NONE} -- Router/administration + + setup_webapi_router (a_router: WSF_ROUTER; a_api: CMS_API) + -- Setup url dispatching for Current module web API. + -- (note: `a_router` is already based with webapi path prefix). + require + is_initialized: is_initialized + router_has_base_url: a_router.base_url /= Void + deferred + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/service/webapi/cms_webapi_auth_filter.e b/src/service/webapi/cms_webapi_auth_filter.e new file mode 100644 index 0000000..1cff400 --- /dev/null +++ b/src/service/webapi/cms_webapi_auth_filter.e @@ -0,0 +1,15 @@ +note + description: "Summary description for {CMS_WEBAPI_AUTH_FILTER}." + date: "$Date$" + revision: "$Revision$" + +deferred class + CMS_WEBAPI_AUTH_FILTER + +inherit + CMS_AUTH_FILTER + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/service/webapi/cms_webapi_handler.e b/src/service/webapi/cms_webapi_handler.e new file mode 100644 index 0000000..6d9a4b5 --- /dev/null +++ b/src/service/webapi/cms_webapi_handler.e @@ -0,0 +1,100 @@ +note + description: "[ + Objects that ... + ]" + author: "$Author$" + date: "$Date$" + revision: "$Revision$" + +deferred class + CMS_WEBAPI_HANDLER + +inherit + WSF_HANDLER + + CMS_API_ACCESS + + CMS_ENCODERS + + REFACTORING_HELPER + +feature {NONE} -- Initialization + + make (a_api: CMS_API) + -- Initialize Current handler with `a_api'. + do + api := a_api + end + +feature -- API Service + + api: CMS_API + +feature -- Factory + + new_webapi_response (req: WSF_REQUEST; res: WSF_RESPONSE): HM_WEBAPI_RESPONSE + do +-- create {MD_WEBAPI_RESPONSE} Result.make (req, res, api) + create {JSON_WEBAPI_RESPONSE} Result.make (req, res, api) + end + + new_wepapi_error_response (msg: detachable READABLE_STRING_GENERAL; req: WSF_REQUEST; res: WSF_RESPONSE): HM_WEBAPI_RESPONSE + do + Result := new_webapi_response (req, res) + if msg /= Void then + Result.add_string_field ("error", msg) + else + Result.add_string_field ("error", "True") + end + end + + send_not_found (m: detachable READABLE_STRING_GENERAL; req: WSF_REQUEST; res: WSF_RESPONSE) + local + rep: HM_WEBAPI_RESPONSE + do + if m /= Void then + rep := new_wepapi_error_response (m, req, res) + else + rep := new_wepapi_error_response ("Not found", req, res) + end + rep.set_status_code ({HTTP_STATUS_CODE}.not_found) + rep.execute + end + + send_access_denied (m: detachable READABLE_STRING_GENERAL; req: WSF_REQUEST; res: WSF_RESPONSE) + local + rep: HM_WEBAPI_RESPONSE + do + if m /= Void then + rep := new_wepapi_error_response (m, req, res) + else + rep := new_wepapi_error_response ("Access denied", req, res) + end + rep.set_status_code ({HTTP_STATUS_CODE}.user_access_denied) + rep.execute + end + + send_bad_request (m: detachable READABLE_STRING_GENERAL; req: WSF_REQUEST; res: WSF_RESPONSE) + local + rep: HM_WEBAPI_RESPONSE + do + if m /= Void then + rep := new_wepapi_error_response (m, req, res) + else + rep := new_wepapi_error_response ("Bad request", req, res) + end + rep.set_status_code ({HTTP_STATUS_CODE}.bad_request) + rep.execute + end + +feature {NONE} -- Builder + + add_user_links_to (u: CMS_USER; rep: HM_WEBAPI_RESPONSE) + do + rep.add_link ("account", "user/" + u.id.out, rep.api.webapi_path ("/user/" + u.id.out)) + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/src/service/webapi/cms_with_webapi.e b/src/service/webapi/cms_with_webapi.e new file mode 100644 index 0000000..1366ae1 --- /dev/null +++ b/src/service/webapi/cms_with_webapi.e @@ -0,0 +1,36 @@ +note + description: "Interface providing webapi module." + date: "$Date$" + revision: "$Revision$" + +deferred class + CMS_WITH_WEBAPI + +feature -- Administration + + module_webapi: like webapi + -- Associated web api module. + do + Result := internal_module_webapi + if Result = Void then + Result := webapi + internal_module_webapi := Result + end + end + +feature {NONE} -- Implementation + + internal_module_webapi: detachable like module_webapi + -- Cached version of `module_webapi`. + +feature {NONE} -- Web API + + webapi: CMS_MODULE_WEBAPI [CMS_MODULE] + -- Web API module. + deferred + end + +note + copyright: "2011-2017, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" + license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" +end diff --git a/themes/admin/assets/css/style.css b/themes/admin/assets/css/style.css new file mode 100644 index 0000000..7b0a705 --- /dev/null +++ b/themes/admin/assets/css/style.css @@ -0,0 +1,107 @@ +div { + background-color: #ffdddd; +} + +ul.horizontal li { + display: inline-block; +} + +#header #primary.menu ul li { + color: #555; + background-color: #fff; + padding: 10px; + margin: 0; +} +#header #primary.menu ul li a { + color: #555; + text-decoration: none; +} +#header #primary.menu ul li a:hover { + color: black; +} +#header #primary.menu ul.horizontal { + border-bottom: solid 1px #ddd; +} +#header #primary.menu ul.horizontal li { + border-top: solid 3px #fff; +} +#header #primary.menu ul.horizontal li:hover { + background-color: #ffe; + border-top: solid 3px #999; +} +#header #primary.menu ul.horizontal li.active { + font-weight: bold; + border-top: solid 3px #ddd; + background-color: #ddd; +} +#header #primary.menu ul.horizontal li.active:hover { + border-top: solid 3px blue; +} + +#content { + margin-left: 20px; +} +#content #highlighted { + position: relative; + border: solid 1px #ddd; + background-color: #ffc; + width: 70%; + left: 15%; + right: 15%; + padding: 5px; + font-style: italic; +} +#content .preview { + border: solid 1px red; +} + +.sidebar { + padding: 5px; + margin: 3px; + /* border: solid 1px #ccc; */ +} +.sidebar#sidebar_first { + width: 250px; + position: fixed; + top: 45px; + left: 0; + bottom: 0; + width: 200px; + border-right: solid 1px #ddd; +} +.sidebar#sidebar_second { + width: 250px; + float: right; +} +.sidebar + .main { + margin-left: 200px; +} + +#primary-tabs ul.horizontal { + list-style-type: none; +} +#primary-tabs ul.horizontal li { + display: inline; + padding: 2px 5px; + border: solid 1px #ccf; +} +#primary-tabs ul.horizontal li.active { + border-color: #99f #99f #ddd; + border-style: solid solid none; + border-width: 2px 1px 0; + padding: 2px 7px 1px; +} + +#message li.error { + background-color: #f99; + border: solid 1px red; + padding: 5px 2px 5px 2px; +} + +table.with_border thead td { + font-weight: bold; +} +table.with_border td { + border: solid 1px #ccc; + padding: 2px 5px 2px 5px; +} diff --git a/themes/admin/assets/favicon.ico b/themes/admin/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a41a15974bacf503bd3fb2873f0c436cf07efeff GIT binary patch literal 994 zcmV<810DPT0096203aX$0096X0L%jb02TlM0EtjeM-2)Z3IG5A4M|8uQUCw|AOHXW zAP5Ek0047(dh`GQ1CvQaK~z}7?blhTR#g-S@Smuenr1x|(Fo>TH;2$NOCK`H1`RR_ zi3-vz6eAj-!GsDbx)n(e(WFrrjgk#QWncvvl;l7qN!GQLk|fNs^m=;vAK2V9zWa5d z9@?@6a2cjyQ+V{nbbO27yZRA5un6nXjtY-r5Z=nT z81wNBCUzzB6LB{#!9IM4DVT{JcoB2(6Gle3cHD=9xU%%X~C` zEcTwR)z@sBf(x)4{jdzJ5d=15JSpQuti&OB5C^9>ZTHXdNmTm~wqY37SkNresZ%IsN&b#gMnS~fL>v|BEk&7tvJ89dlpuqCE(PplgxW1pxlTf(`$EH zhpY1P%{T>z;TJrNBN7)@;8@($jOteg)n6B?-xmg>f`Q3-<8-_QE^?6vD`E7W%*sJp0 zt(n)~Ul7?HpYDNAi}jrt8~|2$8&_xkF)m6l8q`_lgOb6j0BuZc{z|-{DiYAb;(J%r zJvrw-jG6j&R)E(-@R0&&HIo^I%3BIEEe{3;;GN?9^8JO>zia0P!BcTUUeUYQUliZ& z9z3i{rW=kYlB|9x&M)iC%X=ni49Uf6KYdrg)-$i2og$>(|ER!0dt$*~eEUGHchuM# z+ifa(8}h^-VuSz`x`^Eq1Gn_MMb>cW z{F+V6rS-Trhr3u QJOBUy07*qoM6N<$f+)[^>]*|#([\w-]*))$/,k=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,E=/^[\],:{}\s]*$/,S=/(?:^|:|,)(?:\s*\[)+/g,A=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,j=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,D=/^-ms-/,L=/-([\da-z])/gi,H=function(e,t){return t.toUpperCase()},q=function(e){(a.addEventListener||"load"===e.type||"complete"===a.readyState)&&(_(),x.ready())},_=function(){a.addEventListener?(a.removeEventListener("DOMContentLoaded",q,!1),e.removeEventListener("load",q,!1)):(a.detachEvent("onreadystatechange",q),e.detachEvent("onload",q))};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,n,r){var i,o;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof x?n[0]:n,x.merge(this,x.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:a,!0)),k.test(i[1])&&x.isPlainObject(n))for(i in n)x.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(o=a.getElementById(i[2]),o&&o.parentNode){if(o.id!==i[2])return r.find(e);this.length=1,this[0]=o}return this.context=a,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return g.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(g.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},l=1,u=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},l=2),"object"==typeof s||x.isFunction(s)||(s={}),u===l&&(s=this,--l);u>l;l++)if(null!=(o=arguments[l]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(x.isPlainObject(r)||(n=x.isArray(r)))?(n?(n=!1,a=e&&x.isArray(e)?e:[]):a=e&&x.isPlainObject(e)?e:{},s[i]=x.extend(c,a,r)):r!==t&&(s[i]=r));return s},x.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=l),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){if(e===!0?!--x.readyWait:!x.isReady){if(!a.body)return setTimeout(x.ready);x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(a,[x]),x.fn.trigger&&x(a).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray||function(e){return"array"===x.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?c[y.call(e)]||"object":typeof e},isPlainObject:function(e){var n;if(!e||"object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!v.call(e,"constructor")&&!v.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}if(x.support.ownLast)for(n in e)return v.call(e,n);for(n in e);return n===t||v.call(e,n)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||a;var r=k.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=x.trim(n),n&&E.test(n.replace(A,"@").replace(j,"]").replace(S,"")))?Function("return "+n)():(x.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||x.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&x.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(D,"ms-").replace(L,H)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:b&&!b.call("\ufeff\u00a0")?function(e){return null==e?"":b.call(e)}:function(e){return null==e?"":(e+"").replace(C,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(m)return m.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return d.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),x.isFunction(e)?(r=g.call(arguments,2),i=function(){return e.apply(n||this,r.concat(g.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):t},access:function(e,n,r,i,o,a,s){var l=0,u=e.length,c=null==r;if("object"===x.type(r)){o=!0;for(l in r)x.access(e,n,l,r[l],!0,a,s)}else if(i!==t&&(o=!0,x.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(x(e),n)})),n))for(;u>l;l++)n(e[l],r,s?i:i.call(e[l],l,n(e[l],r)));return o?e:c?n.call(e):u?n(e[0],r):a},now:function(){return(new Date).getTime()},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),x.ready.promise=function(t){if(!n)if(n=x.Deferred(),"complete"===a.readyState)setTimeout(x.ready);else if(a.addEventListener)a.addEventListener("DOMContentLoaded",q,!1),e.addEventListener("load",q,!1);else{a.attachEvent("onreadystatechange",q),e.attachEvent("onload",q);var r=!1;try{r=null==e.frameElement&&a.documentElement}catch(i){}r&&r.doScroll&&function o(){if(!x.isReady){try{r.doScroll("left")}catch(e){return setTimeout(o,50)}_(),x.ready()}}()}return n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){c["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=x(a),function(e,t){var n,r,i,o,a,s,l,u,c,p,f,d,h,g,m,y,v,b="sizzle"+-new Date,w=e.document,T=0,C=0,N=st(),k=st(),E=st(),S=!1,A=function(e,t){return e===t?(S=!0,0):0},j=typeof t,D=1<<31,L={}.hasOwnProperty,H=[],q=H.pop,_=H.push,M=H.push,O=H.slice,F=H.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},B="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",P="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=R.replace("w","w#"),$="\\["+P+"*("+R+")"+P+"*(?:([*^$|!~]?=)"+P+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+P+"*\\]",I=":("+R+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+P+"+|((?:^|[^\\\\])(?:\\\\.)*)"+P+"+$","g"),X=RegExp("^"+P+"*,"+P+"*"),U=RegExp("^"+P+"*([>+~]|"+P+")"+P+"*"),V=RegExp(P+"*[+~]"),Y=RegExp("="+P+"*([^\\]'\"]*)"+P+"*\\]","g"),J=RegExp(I),G=RegExp("^"+W+"$"),Q={ID:RegExp("^#("+R+")"),CLASS:RegExp("^\\.("+R+")"),TAG:RegExp("^("+R.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+P+"*(even|odd|(([+-]|)(\\d*)n|)"+P+"*(?:([+-]|)"+P+"*(\\d+)|))"+P+"*\\)|)","i"),bool:RegExp("^(?:"+B+")$","i"),needsContext:RegExp("^"+P+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+P+"*((?:-\\d)?\\d*)"+P+"*\\)|)(?=[^-]|$)","i")},K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,et=/^(?:input|select|textarea|button)$/i,tt=/^h\d$/i,nt=/'|\\/g,rt=RegExp("\\\\([\\da-f]{1,6}"+P+"?|("+P+")|.)","ig"),it=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{M.apply(H=O.call(w.childNodes),w.childNodes),H[w.childNodes.length].nodeType}catch(ot){M={apply:H.length?function(e,t){_.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function at(e,t,n,i){var o,a,s,l,u,c,d,m,y,x;if((t?t.ownerDocument||t:w)!==f&&p(t),t=t||f,n=n||[],!e||"string"!=typeof e)return n;if(1!==(l=t.nodeType)&&9!==l)return[];if(h&&!i){if(o=Z.exec(e))if(s=o[1]){if(9===l){if(a=t.getElementById(s),!a||!a.parentNode)return n;if(a.id===s)return n.push(a),n}else if(t.ownerDocument&&(a=t.ownerDocument.getElementById(s))&&v(t,a)&&a.id===s)return n.push(a),n}else{if(o[2])return M.apply(n,t.getElementsByTagName(e)),n;if((s=o[3])&&r.getElementsByClassName&&t.getElementsByClassName)return M.apply(n,t.getElementsByClassName(s)),n}if(r.qsa&&(!g||!g.test(e))){if(m=d=b,y=t,x=9===l&&e,1===l&&"object"!==t.nodeName.toLowerCase()){c=mt(e),(d=t.getAttribute("id"))?m=d.replace(nt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",u=c.length;while(u--)c[u]=m+yt(c[u]);y=V.test(e)&&t.parentNode||t,x=c.join(",")}if(x)try{return M.apply(n,y.querySelectorAll(x)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,n,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>o.cacheLength&&delete t[e.shift()],t[n]=r}return t}function lt(e){return e[b]=!0,e}function ut(e){var t=f.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ct(e,t){var n=e.split("|"),r=e.length;while(r--)o.attrHandle[n[r]]=t}function pt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function dt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return lt(function(t){return t=+t,lt(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}s=at.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},r=at.support={},p=at.setDocument=function(e){var n=e?e.ownerDocument||e:w,i=n.defaultView;return n!==f&&9===n.nodeType&&n.documentElement?(f=n,d=n.documentElement,h=!s(n),i&&i.attachEvent&&i!==i.top&&i.attachEvent("onbeforeunload",function(){p()}),r.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),r.getElementsByTagName=ut(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),r.getElementsByClassName=ut(function(e){return e.innerHTML="
",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),r.getById=ut(function(e){return d.appendChild(e).id=b,!n.getElementsByName||!n.getElementsByName(b).length}),r.getById?(o.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){return e.getAttribute("id")===t}}):(delete o.find.ID,o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),o.find.TAG=r.getElementsByTagName?function(e,n){return typeof n.getElementsByTagName!==j?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},o.find.CLASS=r.getElementsByClassName&&function(e,n){return typeof n.getElementsByClassName!==j&&h?n.getElementsByClassName(e):t},m=[],g=[],(r.qsa=K.test(n.querySelectorAll))&&(ut(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||g.push("\\["+P+"*(?:value|"+B+")"),e.querySelectorAll(":checked").length||g.push(":checked")}),ut(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&g.push("[*^$]="+P+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(r.matchesSelector=K.test(y=d.webkitMatchesSelector||d.mozMatchesSelector||d.oMatchesSelector||d.msMatchesSelector))&&ut(function(e){r.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),m.push("!=",I)}),g=g.length&&RegExp(g.join("|")),m=m.length&&RegExp(m.join("|")),v=K.test(d.contains)||d.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},A=d.compareDocumentPosition?function(e,t){if(e===t)return S=!0,0;var i=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t);return i?1&i||!r.sortDetached&&t.compareDocumentPosition(e)===i?e===n||v(w,e)?-1:t===n||v(w,t)?1:c?F.call(c,e)-F.call(c,t):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return S=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:c?F.call(c,e)-F.call(c,t):0;if(o===a)return pt(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?pt(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},n):f},at.matches=function(e,t){return at(e,null,null,t)},at.matchesSelector=function(e,t){if((e.ownerDocument||e)!==f&&p(e),t=t.replace(Y,"='$1']"),!(!r.matchesSelector||!h||m&&m.test(t)||g&&g.test(t)))try{var n=y.call(e,t);if(n||r.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(i){}return at(t,f,null,[e]).length>0},at.contains=function(e,t){return(e.ownerDocument||e)!==f&&p(e),v(e,t)},at.attr=function(e,n){(e.ownerDocument||e)!==f&&p(e);var i=o.attrHandle[n.toLowerCase()],a=i&&L.call(o.attrHandle,n.toLowerCase())?i(e,n,!h):t;return a===t?r.attributes||!h?e.getAttribute(n):(a=e.getAttributeNode(n))&&a.specified?a.value:null:a},at.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},at.uniqueSort=function(e){var t,n=[],i=0,o=0;if(S=!r.detectDuplicates,c=!r.sortStable&&e.slice(0),e.sort(A),S){while(t=e[o++])t===e[o]&&(i=n.push(o));while(i--)e.splice(n[i],1)}return e},a=at.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=a(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=a(t);return n},o=at.selectors={cacheLength:50,createPseudo:lt,match:Q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(rt,it),e[3]=(e[4]||e[5]||"").replace(rt,it),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||at.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&at.error(e[0]),e},PSEUDO:function(e){var n,r=!e[5]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]&&e[4]!==t?e[2]=e[4]:r&&J.test(r)&&(n=mt(r,!0))&&(n=r.indexOf(")",r.length-n)-r.length)&&(e[0]=e[0].slice(0,n),e[2]=r.slice(0,n)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(rt,it).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=RegExp("(^|"+P+")"+e+"("+P+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=at.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!l&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[b]||(m[b]={}),u=c[e]||[],d=u[0]===T&&u[1],f=u[0]===T&&u[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[T,d,f];break}}else if(v&&(u=(t[b]||(t[b]={}))[e])&&u[0]===T)f=u[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[b]||(p[b]={}))[e]=[T,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=o.pseudos[e]||o.setFilters[e.toLowerCase()]||at.error("unsupported pseudo: "+e);return r[b]?r(t):r.length>1?(n=[e,e,"",t],o.setFilters.hasOwnProperty(e.toLowerCase())?lt(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=F.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:lt(function(e){var t=[],n=[],r=l(e.replace(z,"$1"));return r[b]?lt(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:lt(function(e){return function(t){return at(e,t).length>0}}),contains:lt(function(e){return function(t){return(t.textContent||t.innerText||a(t)).indexOf(e)>-1}}),lang:lt(function(e){return G.test(e||"")||at.error("unsupported lang: "+e),e=e.replace(rt,it).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===d},focus:function(e){return e===f.activeElement&&(!f.hasFocus||f.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!o.pseudos.empty(e)},header:function(e){return tt.test(e.nodeName)},input:function(e){return et.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},o.pseudos.nth=o.pseudos.eq;for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})o.pseudos[n]=ft(n);for(n in{submit:!0,reset:!0})o.pseudos[n]=dt(n);function gt(){}gt.prototype=o.filters=o.pseudos,o.setFilters=new gt;function mt(e,t){var n,r,i,a,s,l,u,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,l=[],u=o.preFilter;while(s){(!n||(r=X.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),l.push(i=[])),n=!1,(r=U.exec(s))&&(n=r.shift(),i.push({value:n,type:r[0].replace(z," ")}),s=s.slice(n.length));for(a in o.filter)!(r=Q[a].exec(s))||u[a]&&!(r=u[a](r))||(n=r.shift(),i.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?at.error(e):k(e,l).slice(0)}function yt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function vt(e,t,n){var r=t.dir,o=n&&"parentNode"===r,a=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||o)return e(t,n,i)}:function(t,n,s){var l,u,c,p=T+" "+a;if(s){while(t=t[r])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[r])if(1===t.nodeType||o)if(c=t[b]||(t[b]={}),(u=c[r])&&u[0]===p){if((l=u[1])===!0||l===i)return l===!0}else if(u=c[r]=[p],u[1]=e(t,n,s)||i,u[1]===!0)return!0}}function bt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,a=[],s=0,l=e.length,u=null!=t;for(;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function wt(e,t,n,r,i,o){return r&&!r[b]&&(r=wt(r)),i&&!i[b]&&(i=wt(i,o)),lt(function(o,a,s,l){var u,c,p,f=[],d=[],h=a.length,g=o||Nt(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:xt(g,f,e,s,l),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,l),r){u=xt(y,d),r(u,[],s,l),c=u.length;while(c--)(p=u[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){u=[],c=y.length;while(c--)(p=y[c])&&u.push(m[c]=p);i(null,y=[],u,l)}c=y.length;while(c--)(p=y[c])&&(u=i?F.call(o,p):f[c])>-1&&(o[u]=!(a[u]=p))}}else y=xt(y===a?y.splice(h,y.length):y),i?i(null,a,y,l):M.apply(a,y)})}function Tt(e){var t,n,r,i=e.length,a=o.relative[e[0].type],s=a||o.relative[" "],l=a?1:0,c=vt(function(e){return e===t},s,!0),p=vt(function(e){return F.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;i>l;l++)if(n=o.relative[e[l].type])f=[vt(bt(f),n)];else{if(n=o.filter[e[l].type].apply(null,e[l].matches),n[b]){for(r=++l;i>r;r++)if(o.relative[e[r].type])break;return wt(l>1&&bt(f),l>1&&yt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&Tt(e.slice(l,r)),i>r&&Tt(e=e.slice(r)),i>r&&yt(e))}f.push(n)}return bt(f)}function Ct(e,t){var n=0,r=t.length>0,a=e.length>0,s=function(s,l,c,p,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,C=u,N=s||a&&o.find.TAG("*",d&&l.parentNode||l),k=T+=null==C?1:Math.random()||.1;for(w&&(u=l!==f&&l,i=n);null!=(h=N[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,l,c)){p.push(h);break}w&&(T=k,i=++n)}r&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,r&&b!==v){g=0;while(m=t[g++])m(x,y,l,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=q.call(p));y=xt(y)}M.apply(p,y),w&&!s&&y.length>0&&v+t.length>1&&at.uniqueSort(p)}return w&&(T=k,u=C),x};return r?lt(s):s}l=at.compile=function(e,t){var n,r=[],i=[],o=E[e+" "];if(!o){t||(t=mt(e)),n=t.length;while(n--)o=Tt(t[n]),o[b]?r.push(o):i.push(o);o=E(e,Ct(i,r))}return o};function Nt(e,t,n){var r=0,i=t.length;for(;i>r;r++)at(e,t[r],n);return n}function kt(e,t,n,i){var a,s,u,c,p,f=mt(e);if(!i&&1===f.length){if(s=f[0]=f[0].slice(0),s.length>2&&"ID"===(u=s[0]).type&&r.getById&&9===t.nodeType&&h&&o.relative[s[1].type]){if(t=(o.find.ID(u.matches[0].replace(rt,it),t)||[])[0],!t)return n;e=e.slice(s.shift().value.length)}a=Q.needsContext.test(e)?0:s.length;while(a--){if(u=s[a],o.relative[c=u.type])break;if((p=o.find[c])&&(i=p(u.matches[0].replace(rt,it),V.test(s[0].type)&&t.parentNode||t))){if(s.splice(a,1),e=i.length&&yt(s),!e)return M.apply(n,i),n;break}}}return l(e,f)(i,t,!h,n,V.test(e)),n}r.sortStable=b.split("").sort(A).join("")===b,r.detectDuplicates=S,p(),r.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(f.createElement("div"))}),ut(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||ct("type|href|height|width",function(e,n,r){return r?t:e.getAttribute(n,"type"===n.toLowerCase()?1:2)}),r.attributes&&ut(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||ct("value",function(e,n,r){return r||"input"!==e.nodeName.toLowerCase()?t:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||ct(B,function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&i.specified?i.value:e[n]===!0?n.toLowerCase():null}),x.find=at,x.expr=at.selectors,x.expr[":"]=x.expr.pseudos,x.unique=at.uniqueSort,x.text=at.getText,x.isXMLDoc=at.isXML,x.contains=at.contains}(e);var O={};function F(e){var t=O[e]={};return x.each(e.match(T)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?O[e]||F(e):x.extend({},e);var n,r,i,o,a,s,l=[],u=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=l.length,n=!0;l&&o>a;a++)if(l[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,l&&(u?u.length&&c(u.shift()):r?l=[]:p.disable())},p={add:function(){if(l){var t=l.length;(function i(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&p.has(n)||l.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=l.length:r&&(s=t,c(r))}return this},remove:function(){return l&&x.each(arguments,function(e,t){var r;while((r=x.inArray(t,l,r))>-1)l.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?x.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],o=0,this},disable:function(){return l=u=r=t,this},disabled:function(){return!l},lock:function(){return u=t,r||p.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!l||i&&!u||(t=t||[],t=[e,t.slice?t.slice():t],n?u.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var a=o[0],s=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=g.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?g.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,l,u;if(r>1)for(s=Array(r),l=Array(r),u=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(a(t,u,n)).fail(o.reject).progress(a(t,l,s)):--i;return i||o.resolveWith(u,n),o.promise()}}),x.support=function(t){var n,r,o,s,l,u,c,p,f,d=a.createElement("div");if(d.setAttribute("className","t"),d.innerHTML="
a",n=d.getElementsByTagName("*")||[],r=d.getElementsByTagName("a")[0],!r||!r.style||!n.length)return t;s=a.createElement("select"),u=s.appendChild(a.createElement("option")),o=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t.getSetAttribute="t"!==d.className,t.leadingWhitespace=3===d.firstChild.nodeType,t.tbody=!d.getElementsByTagName("tbody").length,t.htmlSerialize=!!d.getElementsByTagName("link").length,t.style=/top/.test(r.getAttribute("style")),t.hrefNormalized="/a"===r.getAttribute("href"),t.opacity=/^0.5/.test(r.style.opacity),t.cssFloat=!!r.style.cssFloat,t.checkOn=!!o.value,t.optSelected=u.selected,t.enctype=!!a.createElement("form").enctype,t.html5Clone="<:nav>"!==a.createElement("nav").cloneNode(!0).outerHTML,t.inlineBlockNeedsLayout=!1,t.shrinkWrapBlocks=!1,t.pixelPosition=!1,t.deleteExpando=!0,t.noCloneEvent=!0,t.reliableMarginRight=!0,t.boxSizingReliable=!0,o.checked=!0,t.noCloneChecked=o.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!u.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}o=a.createElement("input"),o.setAttribute("value",""),t.input=""===o.getAttribute("value"),o.value="t",o.setAttribute("type","radio"),t.radioValue="t"===o.value,o.setAttribute("checked","t"),o.setAttribute("name","t"),l=a.createDocumentFragment(),l.appendChild(o),t.appendChecked=o.checked,t.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip;for(f in x(t))break;return t.ownLast="0"!==f,x(function(){var n,r,o,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",l=a.getElementsByTagName("body")[0];l&&(n=a.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",l.appendChild(n).appendChild(d),d.innerHTML="
t
",o=d.getElementsByTagName("td"),o[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===o[0].offsetHeight,o[0].style.display="",o[1].style.display="none",t.reliableHiddenOffsets=p&&0===o[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",x.swap(l,null!=l.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===d.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(a.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="
",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(l.style.zoom=1)),l.removeChild(n),n=d=o=r=null)}),n=s=l=u=r=o=null,t +}({});var B=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;function R(e,n,r,i){if(x.acceptData(e)){var o,a,s=x.expando,l=e.nodeType,u=l?x.cache:e,c=l?e[s]:e[s]&&s;if(c&&u[c]&&(i||u[c].data)||r!==t||"string"!=typeof n)return c||(c=l?e[s]=p.pop()||x.guid++:s),u[c]||(u[c]=l?{}:{toJSON:x.noop}),("object"==typeof n||"function"==typeof n)&&(i?u[c]=x.extend(u[c],n):u[c].data=x.extend(u[c].data,n)),a=u[c],i||(a.data||(a.data={}),a=a.data),r!==t&&(a[x.camelCase(n)]=r),"string"==typeof n?(o=a[n],null==o&&(o=a[x.camelCase(n)])):o=a,o}}function W(e,t,n){if(x.acceptData(e)){var r,i,o=e.nodeType,a=o?x.cache:e,s=o?e[x.expando]:x.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){x.isArray(t)?t=t.concat(x.map(t,x.camelCase)):t in r?t=[t]:(t=x.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;while(i--)delete r[t[i]];if(n?!I(r):!x.isEmptyObject(r))return}(n||(delete a[s].data,I(a[s])))&&(o?x.cleanData([e],!0):x.support.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}x.extend({cache:{},noData:{applet:!0,embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?x.cache[e[x.expando]]:e[x.expando],!!e&&!I(e)},data:function(e,t,n){return R(e,t,n)},removeData:function(e,t){return W(e,t)},_data:function(e,t,n){return R(e,t,n,!0)},_removeData:function(e,t){return W(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&x.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),x.fn.extend({data:function(e,n){var r,i,o=null,a=0,s=this[0];if(e===t){if(this.length&&(o=x.data(s),1===s.nodeType&&!x._data(s,"parsedAttrs"))){for(r=s.attributes;r.length>a;a++)i=r[a].name,0===i.indexOf("data-")&&(i=x.camelCase(i.slice(5)),$(s,i,o[i]));x._data(s,"parsedAttrs",!0)}return o}return"object"==typeof e?this.each(function(){x.data(this,e)}):arguments.length>1?this.each(function(){x.data(this,e,n)}):s?$(s,e,x.data(s,e)):null},removeData:function(e){return this.each(function(){x.removeData(this,e)})}});function $(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(P,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:B.test(r)?x.parseJSON(r):r}catch(o){}x.data(e,n,r)}else r=t}return r}function I(e){var t;for(t in e)if(("data"!==t||!x.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}x.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=x._data(e,n),r&&(!i||x.isArray(r)?i=x._data(e,n,x.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),a=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return x._data(e,n)||x._data(e,n,{empty:x.Callbacks("once memory").add(function(){x._removeData(e,t+"queue"),x._removeData(e,n)})})}}),x.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?x.queue(this[0],e):n===t?this:this.each(function(){var t=x.queue(this,e,n);x._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=x.Deferred(),a=this,s=this.length,l=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=x._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(l));return l(),o.promise(n)}});var z,X,U=/[\t\r\n\f]/g,V=/\r/g,Y=/^(?:input|select|textarea|button|object)$/i,J=/^(?:a|area)$/i,G=/^(?:checked|selected)$/i,Q=x.support.getSetAttribute,K=x.support.input;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return e=x.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,l="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,l=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,r=0,o=x(this),a=e.match(T)||[];while(t=a[r++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===i||"boolean"===n)&&(this.className&&x._data(this,"__className__",this.className),this.className=this.className||e===!1?"":x._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(U," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=x.isFunction(e),this.each(function(n){var o;1===this.nodeType&&(o=i?e.call(this,n,x(this).val()):e,null==o?o="":"number"==typeof o?o+="":x.isArray(o)&&(o=x.map(o,function(e){return null==e?"":e+""})),r=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=x.valHooks[o.type]||x.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(V,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;for(;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),a=i.length;while(a--)r=i[a],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,n,r){var o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===i?x.prop(e,n,r):(1===s&&x.isXMLDoc(e)||(n=n.toLowerCase(),o=x.attrHooks[n]||(x.expr.match.bool.test(n)?X:z)),r===t?o&&"get"in o&&null!==(a=o.get(e,n))?a:(a=x.find.attr(e,n),null==a?t:a):null!==r?o&&"set"in o&&(a=o.set(e,r,n))!==t?a:(e.setAttribute(n,r+""),r):(x.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(T);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)?K&&Q||!G.test(n)?e[r]=!1:e[x.camelCase("default-"+n)]=e[r]=!1:x.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!x.isXMLDoc(e),a&&(n=x.propFix[n]||n,o=x.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):Y.test(e.nodeName)||J.test(e.nodeName)&&e.href?0:-1}}}}),X={set:function(e,t,n){return t===!1?x.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&x.propFix[n]||n,n):e[x.camelCase("default-"+n)]=e[n]=!0,n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,n){var r=x.expr.attrHandle[n]||x.find.attr;x.expr.attrHandle[n]=K&&Q||!G.test(n)?function(e,n,i){var o=x.expr.attrHandle[n],a=i?t:(x.expr.attrHandle[n]=t)!=r(e,n,i)?n.toLowerCase():null;return x.expr.attrHandle[n]=o,a}:function(e,n,r){return r?t:e[x.camelCase("default-"+n)]?n.toLowerCase():null}}),K&&Q||(x.attrHooks.value={set:function(e,n,r){return x.nodeName(e,"input")?(e.defaultValue=n,t):z&&z.set(e,n,r)}}),Q||(z={set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},x.expr.attrHandle.id=x.expr.attrHandle.name=x.expr.attrHandle.coords=function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&""!==i.value?i.value:null},x.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&r.specified?r.value:t},set:z.set},x.attrHooks.contenteditable={set:function(e,t,n){z.set(e,""===t?!1:t,n)}},x.each(["width","height"],function(e,n){x.attrHooks[n]={set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}}})),x.support.hrefNormalized||x.each(["href","src"],function(e,t){x.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),x.support.style||(x.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.support.enctype||(x.propFix.enctype="encoding"),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,n){return x.isArray(n)?e.checked=x.inArray(x(e).val(),n)>=0:t}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}function at(){try{return a.activeElement}catch(e){}}x.event={global:{},add:function(e,n,r,o,a){var s,l,u,c,p,f,d,h,g,m,y,v=x._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=x.guid++),(l=v.events)||(l=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof x===i||e&&x.event.triggered===e.type?t:x.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(T)||[""],u=n.length;while(u--)s=rt.exec(n[u])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),g&&(p=x.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=x.event.special[g]||{},d=x.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&x.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=l[g])||(h=l[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),x.event.global[g]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,p,f,d,h,g,m=x.hasData(e)&&x._data(e);if(m&&(c=m.events)){t=(t||"").match(T)||[""],u=t.length;while(u--)if(s=rt.exec(t[u])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=x.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));l&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||x.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)x.event.remove(e,d+t[u],n,r,!0);x.isEmptyObject(c)&&(delete m.handle,x._removeData(e,"events"))}},trigger:function(n,r,i,o){var s,l,u,c,p,f,d,h=[i||a],g=v.call(n,"type")?n.type:n,m=v.call(n,"namespace")?n.namespace.split("."):[];if(u=f=i=i||a,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+x.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),l=0>g.indexOf(":")&&"on"+g,n=n[x.expando]?n:new x.Event(g,"object"==typeof n&&n),n.isTrigger=o?2:3,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:x.makeArray(r,[n]),p=x.event.special[g]||{},o||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!o&&!p.noBubble&&!x.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(u=u.parentNode);u;u=u.parentNode)h.push(u),f=u;f===(i.ownerDocument||a)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((u=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(x._data(u,"events")||{})[n.type]&&x._data(u,"handle"),s&&s.apply(u,r),s=l&&u[l],s&&x.acceptData(u)&&s.apply&&s.apply(u,r)===!1&&n.preventDefault();if(n.type=g,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(h.pop(),r)===!1)&&x.acceptData(i)&&l&&i[g]&&!x.isWindow(i)){f=i[l],f&&(i[l]=null),x.event.triggered=g;try{i[g]()}catch(y){}x.event.triggered=t,f&&(i[l]=f)}return n.result}},dispatch:function(e){e=x.event.fix(e);var n,r,i,o,a,s=[],l=g.call(arguments),u=(x._data(this,"events")||{})[e.type]||[],c=x.event.special[e.type]||{};if(l[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((x.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,l),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],l=n.delegateCount,u=e.target;if(l&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(o=[],a=0;l>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?x(r,this).index(u)>=0:x.find(r,this,null,[u]).length),o[r]&&o.push(i);o.length&&s.push({elem:u,handlers:o})}return n.length>l&&s.push({elem:this,handlers:n.slice(l)}),s},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||a),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,o,s=n.button,l=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||a,o=i.documentElement,r=i.body,e.pageX=n.clientX+(o&&o.scrollLeft||r&&r.scrollLeft||0)-(o&&o.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(o&&o.scrollTop||r&&r.scrollTop||0)-(o&&o.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&l&&(e.relatedTarget=l===e.target?n.toElement:l),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==at()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===at()&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},click:{trigger:function(){return x.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=a.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},x.Event=function(e,n){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&x.extend(this,n),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,t):new x.Event(e,n)},x.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.submitBubbles||(x.event.special.submit={setup:function(){return x.nodeName(this,"form")?!1:(x.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=x.nodeName(n,"input")||x.nodeName(n,"button")?n.form:t;r&&!x._data(r,"submitBubbles")&&(x.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),x._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&x.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return x.nodeName(this,"form")?!1:(x.event.remove(this,"._submit"),t)}}),x.support.changeBubbles||(x.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(x.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),x.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),x.event.simulate("change",this,e,!0)})),!1):(x.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!x._data(t,"changeBubbles")&&(x.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||x.event.simulate("change",this.parentNode,e,!0)}),x._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return x.event.remove(this,"._change"),!Z.test(this.nodeName)}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&a.addEventListener(e,r,!0)},teardown:function(){0===--n&&a.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return x().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=x.guid++)),this.each(function(){x.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,x(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){x.event.remove(this,e,r,n)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?x.event.trigger(e,n,r,!0):t}});var st=/^.[^:#\[\.,]*$/,lt=/^(?:parents|prev(?:Until|All))/,ut=x.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t,n=x(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(x.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e||[],!0))},filter:function(e){return this.pushStack(ft(this,e||[],!1))},is:function(e){return!!ft(this,"string"==typeof e&&ut.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],a=ut.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(a?a.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?x.inArray(this[0],x(e)):x.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(ct[e]||(i=x.unique(i)),lt.test(e)&&(i=i.reverse())),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!x(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(st.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return x.inArray(e,t)>=0!==n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/\s*$/g,At={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:x.support.htmlSerialize?[0,"",""]:[1,"X
","
"]},jt=dt(a),Dt=jt.appendChild(a.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===t?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||a).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(Ft(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&_t(Ft(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&x.cleanData(Ft(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&x.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!x.support.htmlSerialize&&mt.test(e)||!x.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(x.cleanData(Ft(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=d.apply([],e);var r,i,o,a,s,l,u=0,c=this.length,p=this,f=c-1,h=e[0],g=x.isFunction(h);if(g||!(1>=c||"string"!=typeof h||x.support.checkClone)&&Nt.test(h))return this.each(function(r){var i=p.eq(r);g&&(e[0]=h.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(l=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),r=l.firstChild,1===l.childNodes.length&&(l=r),r)){for(a=x.map(Ft(l,"script"),Ht),o=a.length;c>u;u++)i=l,u!==f&&(i=x.clone(i,!0,!0),o&&x.merge(a,Ft(i,"script"))),t.call(this[u],i,u);if(o)for(s=a[a.length-1].ownerDocument,x.map(a,qt),u=0;o>u;u++)i=a[u],kt.test(i.type||"")&&!x._data(i,"globalEval")&&x.contains(s,i)&&(i.src?x._evalUrl(i.src):x.globalEval((i.text||i.textContent||i.innerHTML||"").replace(St,"")));l=r=null}return this}});function Lt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ht(e){return e.type=(null!==x.find.attr(e,"type"))+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function _t(e,t){var n,r=0;for(;null!=(n=e[r]);r++)x._data(n,"globalEval",!t||x._data(t[r],"globalEval"))}function Mt(e,t){if(1===t.nodeType&&x.hasData(e)){var n,r,i,o=x._data(e),a=x._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)x.event.add(t,n,s[n][r])}a.data&&(a.data=x.extend({},a.data))}}function Ot(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!x.support.noCloneEvent&&t[x.expando]){i=x._data(t);for(r in i.events)x.removeEvent(t,r,i.handle);t.removeAttribute(x.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),x.support.html5Clone&&e.innerHTML&&!x.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Ct.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=0,i=[],o=x(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),x(o[r])[t](n),h.apply(i,n.get());return this.pushStack(i)}});function Ft(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||x.nodeName(o,n)?s.push(o):x.merge(s,Ft(o,n));return n===t||n&&x.nodeName(e,n)?x.merge([e],s):s}function Bt(e){Ct.test(e.type)&&(e.defaultChecked=e.checked)}x.extend({clone:function(e,t,n){var r,i,o,a,s,l=x.contains(e.ownerDocument,e);if(x.support.html5Clone||x.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(x.support.noCloneEvent&&x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(r=Ft(o),s=Ft(e),a=0;null!=(i=s[a]);++a)r[a]&&Ot(i,r[a]);if(t)if(n)for(s=s||Ft(e),r=r||Ft(o),a=0;null!=(i=s[a]);a++)Mt(i,r[a]);else Mt(e,o);return r=Ft(o,"script"),r.length>0&&_t(r,!l&&Ft(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,l,u,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===x.type(o))x.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),l=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[l]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!x.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!x.support.tbody){o="table"!==l||xt.test(o)?""!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)x.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u)}x.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),x.support.appendChecked||x.grep(Ft(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===x.inArray(o,r))&&(a=x.contains(o.ownerDocument,o),s=Ft(f.appendChild(o),"script"),a&&_t(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,l=x.expando,u=x.cache,c=x.support.deleteExpando,f=x.event.special;for(;null!=(n=e[s]);s++)if((t||x.acceptData(n))&&(o=n[l],a=o&&u[o])){if(a.events)for(r in a.events)f[r]?x.event.remove(n,r):x.removeEvent(n,r,a.handle); +u[o]&&(delete u[o],c?delete n[l]:typeof n.removeAttribute!==i?n.removeAttribute(l):n[l]=null,p.push(o))}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}}),x.fn.extend({wrapAll:function(e){if(x.isFunction(e))return this.each(function(t){x(this).wrapAll(e.call(this,t))});if(this[0]){var t=x(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var Pt,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+w+")(.*)$","i"),Yt=RegExp("^("+w+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+w+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=x._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=x._data(r,"olddisplay",ln(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&x._data(r,"olddisplay",i?n:x.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}x.fn.extend({css:function(e,n){return x.access(this,function(e,n,r){var i,o,a={},s=0;if(x.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=x.css(e,n[s],!1,o);return a}return r!==t?x.style(e,n,r):x.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){nn(this)?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":x.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,l=x.camelCase(n),u=e.style;if(n=x.cssProps[l]||(x.cssProps[l]=tn(u,l)),s=x.cssHooks[n]||x.cssHooks[l],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:u[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(x.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||x.cssNumber[l]||(r+="px"),x.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(u[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{u[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,l=x.camelCase(n);return n=x.cssProps[l]||(x.cssProps[l]=tn(e.style,l)),s=x.cssHooks[n]||x.cssHooks[l],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||x.isNumeric(o)?o||0:a):a}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s.getPropertyValue(n)||s[n]:t,u=e.style;return s&&(""!==l||x.contains(e.ownerDocument,e)||(l=x.style(e,n)),Yt.test(l)&&Ut.test(n)&&(i=u.width,o=u.minWidth,a=u.maxWidth,u.minWidth=u.maxWidth=u.width=l,l=s.width,u.width=i,u.minWidth=o,u.maxWidth=a)),l}):a.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s[n]:t,u=e.style;return null==l&&u&&u[n]&&(l=u[n]),Yt.test(l)&&!zt.test(n)&&(i=u.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),u.left="fontSize"===n?"1em":l,l=u.pixelLeft+"px",u.left=i,a&&(o.left=a)),""===l?"auto":l});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=x.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=x.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=x.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=x.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=x.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function ln(e){var t=a,n=Gt[e];return n||(n=un(e,t),"none"!==n&&n||(Pt=(Pt||x("