diff --git a/modules/admin/cms_admin_module.e b/modules/admin/cms_admin_module.e index 6c6e8b6..8d83cc9 100644 --- a/modules/admin/cms_admin_module.e +++ b/modules/admin/cms_admin_module.e @@ -105,6 +105,7 @@ feature -- Security Result.force ("admin core caches") Result.force ("clear blocks cache") Result.force ("admin export") + Result.force ("export core") end feature -- Hooks diff --git a/modules/admin/handler/cms_admin_export_handler.e b/modules/admin/handler/cms_admin_export_handler.e index 7dd391e..4380b74 100644 --- a/modules/admin/handler/cms_admin_export_handler.e +++ b/modules/admin/handler/cms_admin_export_handler.e @@ -64,7 +64,12 @@ feature -- Execution fd.is_valid then if attached fd.string_item ("op") as l_op and then l_op.same_string (text_export_all_data) then - create l_exportation_parameters.make (api.site_location.extended ("export").extended ((create {DATE_TIME}.make_now_utc).formatted_out ("yyyy-[0]mm-[0]dd_hh12-[0]mi-[0]ss"))) + if attached fd.string_item ("folder") as l_folder then + create l_exportation_parameters.make (api.site_location.extended ("export").extended (l_folder)) + else + create l_exportation_parameters.make (api.site_location.extended ("export").extended ((create {DATE_TIME}.make_now_utc).formatted_out ("yyyy-[0]mm-[0]dd---hh24-[0]mi-[0]ss"))) + end + l_response.hooks.invoke_export_to (Void, l_exportation_parameters, l_response) l_response.add_notice_message ("All data exported (if allowed)!") create s.make_empty @@ -90,9 +95,16 @@ feature -- Widget exportation_web_form (a_response: CMS_RESPONSE): CMS_FORM local + f_name: WSF_FORM_TEXT_INPUT but: WSF_FORM_SUBMIT_INPUT do create Result.make (a_response.url (a_response.location, Void), "export_all_data") + Result.extend_raw_text ("Export CMS data to ") + create f_name.make_with_text ("folder", (create {DATE_TIME}.make_now_utc).formatted_out ("yyyy-[0]mm-[0]dd---hh24-[0]mi-[0]ss")) + f_name.set_label ("Export folder name") + f_name.set_description ("Folder name under 'exports' folder.") + f_name.set_is_required (True) + Result.extend (f_name) create but.make_with_text ("op", text_export_all_data) Result.extend (but) end diff --git a/modules/blog/cms_blog_module.e b/modules/blog/cms_blog_module.e index b55e22e..0daf7cc 100644 --- a/modules/blog/cms_blog_module.e +++ b/modules/blog/cms_blog_module.e @@ -188,7 +188,10 @@ feature -- Hooks a_export_id_list = Void or else across a_export_id_list as ic some ic.item.same_string ("blog") end then - if attached blog_api as l_blog_api then + if + a_response.has_permissions (<<"export any node", "export blog">>) and then + attached blog_api as l_blog_api + then lst := l_blog_api.blogs_order_created_desc a_export_parameters.log ("Exporting " + lst.count.out + " blogs") across @@ -207,6 +210,30 @@ feature -- Hooks f.put_string (json_to_string (blog_node_to_json (n))) f.close end + -- Revisions. + if + attached node_api as l_node_api and then + attached l_node_api.node_revisions (n) as l_revisions and then l_revisions.count > 1 + then + a_export_parameters.log (n.content_type + " " + l_revisions.count.out + " revisions.") + p := a_export_parameters.location.extended ("nodes").extended (n.content_type).extended (n.id.out) + create d.make_with_path (p) + if not d.exists then + d.recursive_create_dir + end + across + l_revisions as revs_ic + loop + if attached {CMS_BLOG} revs_ic.item as l_blog then + create f.make_with_path (p.extended ("rev-" + n.revision.out).appended_with_extension ("json")) + if not f.exists or else f.is_access_writable then + f.open_write + f.put_string (json_to_string (blog_node_to_json (l_blog))) + end + f.close + end + end + end end end end diff --git a/modules/node/cms_node_module.e b/modules/node/cms_node_module.e index 1476dc3..b6fd250 100644 --- a/modules/node/cms_node_module.e +++ b/modules/node/cms_node_module.e @@ -151,6 +151,7 @@ feature -- Access do Result := Precursor Result.force ("create any node") + Result.force ("export any node") if attached node_api as l_node_api then across @@ -177,6 +178,8 @@ feature -- Access Result.force ("view unpublished " + l_type_name) Result.force ("view revisions own " + l_type_name) + + Result.force ("export " + l_type_name) end end Result.force ("view trash") @@ -375,6 +378,7 @@ feature -- Hooks loop l_node_type := types_ic.item if + a_response.has_permissions (<<"export any node", "export " + l_node_type.name>>) and then l_node_type.name.same_string_general ("page") and then ( a_export_id_list = Void or else across a_export_id_list as ic some ic.item.same_string (l_node_type.name) end @@ -384,17 +388,17 @@ feature -- Hooks -- For now, handle only page from this node module. lst := l_node_api.nodes_of_type (l_node_type) a_export_parameters.log ("Exporting " + lst.count.out + " nodes of type " + l_node_type.name) + p := a_export_parameters.location.extended ("nodes").extended (l_node_type.name) + create d.make_with_path (p) + if not d.exists then + d.recursive_create_dir + end across lst as ic loop n := l_node_api.full_node (ic.item) - a_export_parameters.log (l_node_type.name + " #" + n.id.out) - p := a_export_parameters.location.extended ("nodes").extended (l_node_type.name).extended (n.id.out) - create d.make_with_path (p.parent) - if not d.exists then - d.recursive_create_dir - end - create f.make_with_path (p) + a_export_parameters.log (l_node_type.name + " #" + n.id.out + " rev=" + n.revision.out) + create f.make_with_path (p.extended (n.id.out).appended_with_extension ("json")) if not f.exists or else f.is_access_writable then f.open_write if attached {CMS_PAGE} n as l_page then @@ -404,6 +408,30 @@ feature -- Hooks end f.close end + -- Revisions. + if attached l_node_api.node_revisions (n) as l_revisions and then l_revisions.count > 1 then + a_export_parameters.log (l_node_type.name + " " + l_revisions.count.out + " revisions.") + p := a_export_parameters.location.extended ("nodes").extended (l_node_type.name).extended (n.id.out) + create d.make_with_path (p) + if not d.exists then + d.recursive_create_dir + end + across + l_revisions as revs_ic + loop + n := revs_ic.item + create f.make_with_path (p.extended ("rev-" + n.revision.out).appended_with_extension ("json")) + if not f.exists or else f.is_access_writable then + f.open_write + if attached {CMS_PAGE} n as l_page then + f.put_string (json_to_string (page_node_to_json (l_page))) + else + f.put_string (json_to_string (node_to_json (n))) + end + f.close + end + end + end end end end diff --git a/modules/node/export/cms_export_node_utilities.e b/modules/node/export/cms_export_node_utilities.e index e05360e..c215d52 100644 --- a/modules/node/export/cms_export_node_utilities.e +++ b/modules/node/export/cms_export_node_utilities.e @@ -7,12 +7,12 @@ note class CMS_EXPORT_NODE_UTILITIES - + inherit CMS_EXPORT_JSON_UTILITIES - + feature -- Access - + node_to_json (n: CMS_NODE): JSON_OBJECT local jo,j_author: JSON_OBJECT diff --git a/src/hooks/export/cms_export_json_utilities.e b/src/hooks/export/cms_export_json_utilities.e index 1a68966..83ae583 100644 --- a/src/hooks/export/cms_export_json_utilities.e +++ b/src/hooks/export/cms_export_json_utilities.e @@ -10,6 +10,13 @@ class feature -- Access + put_string_into_json (st: detachable READABLE_STRING_GENERAL; a_key: JSON_STRING; j: JSON_OBJECT) + do + if st /= Void then + j.put_string (st, a_key) + end + end + put_date_into_json (dt: detachable DATE_TIME; a_key: JSON_STRING; j: JSON_OBJECT) local hd: HTTP_DATE @@ -20,7 +27,7 @@ feature -- Access end end - json_to_string (j: JSON_OBJECT): STRING + json_to_string (j: JSON_VALUE): STRING local pp: JSON_PRETTY_STRING_VISITOR do diff --git a/src/persistence/cms_storage_null.e b/src/persistence/cms_storage_null.e index f74a427..5156d25 100644 --- a/src/persistence/cms_storage_null.e +++ b/src/persistence/cms_storage_null.e @@ -80,6 +80,12 @@ feature -- URL aliases do end + path_aliases: STRING_TABLE [READABLE_STRING_8] + -- . + do + create Result.make (0) + end + feature -- Logs save_log (a_log: CMS_LOG) @@ -116,5 +122,12 @@ feature -- Custom end end + custom_values: detachable LIST [TUPLE [name: READABLE_STRING_GENERAL; type: detachable READABLE_STRING_8; value: detachable READABLE_STRING_32]] + -- Values as list of [name, type, value]. + do + end +note + copyright: "2011-2015, 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/persistence/core/cms_core_storage_i.e index ef87eba..661fc46 100644 --- a/src/persistence/core/cms_core_storage_i.e +++ b/src/persistence/core/cms_core_storage_i.e @@ -47,6 +47,11 @@ feature -- URL aliases deferred end + path_aliases: STRING_TABLE [READABLE_STRING_8] + -- All path aliases as a table containing sources indexed by alias. + deferred + end + feature -- Logs save_log (a_log: CMS_LOG) @@ -71,5 +76,12 @@ feature -- Misc deferred end + custom_values: detachable LIST [TUPLE [name: READABLE_STRING_GENERAL; type: detachable READABLE_STRING_8; value: detachable READABLE_STRING_32]] + -- Values as list of [name, type, value]. + deferred + end +note + copyright: "2011-2015, 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_sql_i.e b/src/persistence/core/cms_core_storage_sql_i.e index d0d234a..2968eb8 100644 --- a/src/persistence/core/cms_core_storage_sql_i.e +++ b/src/persistence/core/cms_core_storage_sql_i.e @@ -127,6 +127,35 @@ feature -- URL aliases sql_finalize end + path_aliases: STRING_TABLE [READABLE_STRING_8] + -- All path aliases as a table containing sources indexed by alias. + local + l_source: READABLE_STRING_8 + do + error_handler.reset + create Result.make (5) + sql_query (sql_select_all_path_alias, Void) + if not has_error then + from + sql_start + until + sql_after or has_error + loop + if attached sql_read_string (1) as s_src then + l_source := s_src + if attached sql_read_string (2) as s_alias then + Result.force (l_source, s_alias) + end + end + sql_forth + end + end + sql_finalize + end + + sql_select_all_path_alias: STRING = "SELECT source, alias, lang FROM path_aliases;" + -- SQL select all path aliases. + sql_select_path_alias: STRING = "SELECT source FROM path_aliases WHERE alias=:alias ;" -- SQL select path aliases. @@ -251,6 +280,39 @@ feature -- Misc sql_finalize end + custom_values: detachable LIST [TUPLE [name: READABLE_STRING_GENERAL; type: detachable READABLE_STRING_8; value: detachable READABLE_STRING_32]] + -- Values as list of [name, type, value]. + local + l_type, l_name: READABLE_STRING_8 + l_value: READABLE_STRING_32 + do + error_handler.reset + create {ARRAYED_LIST [TUPLE [name: READABLE_STRING_GENERAL; type: detachable READABLE_STRING_8; value: detachable READABLE_STRING_32]]} Result.make (5) + sql_query (sql_select_all_custom_values, Void) + if not has_error then + from + sql_start + until + sql_after or has_error + loop + if attached sql_read_string (1) as s_type then + l_type := s_type + if attached sql_read_string (2) as s_name then + l_name := s_name + if attached sql_read_string_32 (3) as s_value then + Result.force ([l_name, l_type, s_value]) + end + end + end + sql_forth + end + end + sql_finalize + end + + sql_select_all_custom_values: STRING = "SELECT type, name, value FROM custom_values;" + -- SQL Insert to add a new custom value. + sql_select_custom_value: STRING = "SELECT value FROM custom_values WHERE type=:type AND name=:name;" -- SQL Insert to add a new custom value. diff --git a/src/persistence/user/cms_user_storage_sql_i.e b/src/persistence/user/cms_user_storage_sql_i.e index 2a59b30..640d6ef 100644 --- a/src/persistence/user/cms_user_storage_sql_i.e +++ b/src/persistence/user/cms_user_storage_sql_i.e @@ -829,6 +829,7 @@ feature {NONE} -- Implementation: User end fetch_user: detachable CMS_USER + -- Fetch user from fields: 1:uid, 2:name, 3:password, 4:salt, 5:email, 6:status, 7:created, 8:signed. local l_id: INTEGER_64 l_name: detachable READABLE_STRING_32 @@ -919,10 +920,10 @@ feature {NONE} -- Sql Queries: USER Select_user_by_name: STRING = "SELECT * FROM users WHERE name =:name;" -- Retrieve user by name if exists. - Sql_select_recent_users: STRING = "SELECT * FROM users ORDER BY uid DESC, created DESC LIMIT :rows OFFSET :offset ;" + Sql_select_recent_users: STRING = "SELECT uid, name, password, salt, email, status, created, signed FROM users ORDER BY uid DESC, created DESC LIMIT :rows OFFSET :offset ;" -- Retrieve recent users - Select_user_by_email: STRING = "SELECT * FROM users WHERE email =:email;" + Select_user_by_email: STRING = "SELECT uid, name, password, salt, email, status, created, signed FROM users WHERE email =:email;" -- Retrieve user by email if exists. Select_salt_by_username: STRING = "SELECT salt FROM users WHERE name =:name;" diff --git a/src/service/cms_api.e b/src/service/cms_api.e index 964221a..2338e06 100644 --- a/src/service/cms_api.e +++ b/src/service/cms_api.e @@ -9,6 +9,10 @@ class inherit ANY + CMS_HOOK_EXPORT + + CMS_EXPORT_JSON_UTILITIES + REFACTORING_HELPER CMS_ENCODERS @@ -316,6 +320,14 @@ feature -- Query: API Result := l_api end +feature -- Hooks + + register_hooks (a_hooks: CMS_HOOK_CORE_MANAGER) + -- Register hooks associated with the cms core. + do + a_hooks.subscribe_to_export_hook (Current) + end + feature -- Path aliases is_valid_path_alias (a_alias: READABLE_STRING_8): BOOLEAN @@ -581,6 +593,113 @@ feature -- Environment/ modules and theme Result := module_configuration_by_name (a_module.name, a_name) end +feature -- Hook + + export_to (a_export_id_list: detachable ITERABLE [READABLE_STRING_GENERAL]; a_export_parameters: CMS_EXPORT_PARAMETERS; a_response: CMS_RESPONSE) + -- . + local + p: PATH + d: DIRECTORY + ja: JSON_ARRAY + jobj,jo,j: JSON_OBJECT + f: PLAIN_TEXT_FILE + u: CMS_USER + s: STRING_32 + do + if attached a_response.has_permissions (<<"admin export", "export core">>) then + if a_export_id_list = Void then -- Include everything + p := a_export_parameters.location.extended ("core") + create d.make_with_path (p) + if not d.exists then + d.recursive_create_dir + end + + -- path_aliases export. + a_export_parameters.log ("Exporting path_aliases") + create jo.make_empty + across storage.path_aliases as ic loop + jo.put_string (ic.item, ic.key) + end + create f.make_with_path (p.extended ("path_aliases.json")) + f.create_read_write + f.put_string (json_to_string (jo)) + f.close + + -- custom_values export. + if attached storage.custom_values as lst then + a_export_parameters.log ("Exporting custom_values") + create ja.make_empty + across + lst as ic + loop + create j.make_empty + if attached ic.item.type as l_type then + j.put_string (l_type, "type") + end + j.put_string (ic.item.name, "name") + if attached ic.item.type as l_value then + j.put_string (l_value, "value") + end + ja.extend (j) + end + create f.make_with_path (p.extended ("custom_values.json")) + f.create_read_write + f.put_string (json_to_string (ja)) + f.close + end + + -- users export. + a_export_parameters.log ("Exporting users") + create jo.make_empty + + create jobj.make_empty + across user_api.recent_users (create {CMS_DATA_QUERY_PARAMETERS}.make (0, user_api.users_count.as_natural_32)) as ic loop + u := ic.item + create j.make_empty + j.put_string (u.name, "name") + j.put_integer (u.status, "status") + put_string_into_json (u.email, "email", j) + put_string_into_json (u.password, "password", j) + put_string_into_json (u.hashed_password, "hashed_password", j) + put_date_into_json (u.creation_date, "creation_date", j) + put_date_into_json (u.last_login_date, "last_login_date", j) + if attached u.roles as l_roles then + create ja.make (l_roles.count) + across + l_roles as roles_ic + loop + ja.extend (create {JSON_STRING}.make_from_string_32 ({STRING_32} " %"" + roles_ic.item.name + {STRING_32} "%" #" + roles_ic.item.id.out)) + end + j.put (ja, "roles") + end + jobj.put (j, u.id.out) + end + jo.put (jobj, "users") + + create jobj.make_empty + across user_api.roles as ic loop + create j.make_empty + j.put_string (ic.item.name, "name") + if attached ic.item.permissions as l_perms then + create ja.make (l_perms.count) + across + l_perms as perms_ic + loop + ja.extend (create {JSON_STRING}.make_from_string (perms_ic.item)) + end + j.put (ja, "permissions") + end + jobj.put (j, ic.item.id.out) + end + jo.put (jobj, "roles") + create f.make_with_path (p.extended ("users.json")) + f.create_read_write + f.put_string (json_to_string (jo)) + f.close + end + end + end + note copyright: "2011-2015, 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/response/cms_response.e b/src/service/response/cms_response.e index d8cf39e..8f34765 100644 --- a/src/service/response/cms_response.e +++ b/src/service/response/cms_response.e @@ -71,6 +71,7 @@ feature {NONE} -- Initialization l_module: CMS_MODULE l_enabled_modules: CMS_MODULE_COLLECTION do + api.register_hooks (hooks) l_enabled_modules := api.enabled_modules across l_enabled_modules as ic