Compare commits
14 Commits
es_rev9823
...
es_rev9828
| Author | SHA1 | Date | |
|---|---|---|---|
| 0813abe0bb | |||
| 1094acb3ec | |||
| e7c9a54f3f | |||
| bbbdac12c8 | |||
|
|
22528315cb | ||
|
|
090a48eb85 | ||
|
|
e05c4dca3a | ||
|
|
2255fcc0f6 | ||
| e50fb6959e | |||
|
|
3b88c746a1 | ||
|
|
fa8ef44a4a | ||
|
|
068943734f | ||
|
|
089179e60e | ||
|
|
c25590c9cd |
@@ -31,6 +31,7 @@
|
|||||||
<library name="cms_node_module" location="..\..\modules\node\node-safe.ecf" readonly="false"/>
|
<library name="cms_node_module" location="..\..\modules\node\node-safe.ecf" readonly="false"/>
|
||||||
<library name="cms_taxnomy_module" location="..\..\modules\taxonomy\taxonomy-safe.ecf" readonly="false"/>
|
<library name="cms_taxnomy_module" location="..\..\modules\taxonomy\taxonomy-safe.ecf" readonly="false"/>
|
||||||
<library name="cms_oauth_20_module" location="..\..\modules\oauth20\oauth20-safe.ecf" readonly="false"/>
|
<library name="cms_oauth_20_module" location="..\..\modules\oauth20\oauth20-safe.ecf" readonly="false"/>
|
||||||
|
<library name="cms_session_auth_module" location="..\..\modules\session_auth\cms_session_auth-safe.ecf" readonly="false"/>
|
||||||
<library name="cms_openid_module" location="..\..\modules\openid\openid-safe.ecf" readonly="false"/>
|
<library name="cms_openid_module" location="..\..\modules\openid\openid-safe.ecf" readonly="false"/>
|
||||||
<library name="cms_recent_changes_module" location="..\..\modules\recent_changes\recent_changes-safe.ecf" readonly="false"/>
|
<library name="cms_recent_changes_module" location="..\..\modules\recent_changes\recent_changes-safe.ecf" readonly="false"/>
|
||||||
<library name="persistence_sqlite3" location="..\..\library\persistence\sqlite3\sqlite3-safe.ecf" readonly="false">
|
<library name="persistence_sqlite3" location="..\..\library\persistence\sqlite3\sqlite3-safe.ecf" readonly="false">
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
CREATE TABLE session_auth (
|
||||||
|
`uid` INTEGER PRIMARY KEY NOT NULL CHECK(`uid`>=0),
|
||||||
|
`access_token` TEXT NOT NULL,
|
||||||
|
`created` DATETIME NOT NULL,
|
||||||
|
CONSTRAINT `uid`
|
||||||
|
UNIQUE(`uid`),
|
||||||
|
CONSTRAINT `access_token`
|
||||||
|
UNIQUE(`access_token`)
|
||||||
|
);
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<div class="primary-tabs">
|
||||||
|
{unless isset="$user"}
|
||||||
|
<h3>Login or <a href="{$site_url/}account/roc-register">Register</a></h3>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<form name="cms_session_auth" action="{$site_url/}account/login-with-session" method="POST">
|
||||||
|
<div>
|
||||||
|
<input type="text" name="username" id="username" required value="{$username/}">
|
||||||
|
<label>Username</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input type="password" name="password" id="password" required >
|
||||||
|
<label>Password</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<a href="{$site_url/}account/new-password">Forgot password?</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/unless}
|
||||||
|
{if isset=$error}
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<strong>{$error/}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -89,6 +89,9 @@ feature -- CMS modules
|
|||||||
|
|
||||||
create {GOOGLE_CUSTOM_SEARCH_MODULE} m.make
|
create {GOOGLE_CUSTOM_SEARCH_MODULE} m.make
|
||||||
a_setup.register_module (m)
|
a_setup.register_module (m)
|
||||||
|
|
||||||
|
create {CMS_SESSION_AUTH_MODULE} m.make
|
||||||
|
a_setup.register_module (m)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ class
|
|||||||
inherit
|
inherit
|
||||||
CMS_MODULE
|
CMS_MODULE
|
||||||
redefine
|
redefine
|
||||||
setup_hooks
|
setup_hooks,
|
||||||
|
permissions
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
CMS_HOOK_AUTO_REGISTER
|
CMS_HOOK_AUTO_REGISTER
|
||||||
|
|
||||||
CMS_HOOK_VALUE_TABLE_ALTER
|
CMS_HOOK_VALUE_TABLE_ALTER
|
||||||
@@ -52,6 +52,13 @@ feature -- Access
|
|||||||
|
|
||||||
name: STRING = "auth"
|
name: STRING = "auth"
|
||||||
|
|
||||||
|
permissions: LIST [READABLE_STRING_8]
|
||||||
|
-- List of permission ids, used by this module, and declared.
|
||||||
|
do
|
||||||
|
Result := Precursor
|
||||||
|
Result.force ("account register")
|
||||||
|
end
|
||||||
|
|
||||||
feature -- Access: docs
|
feature -- Access: docs
|
||||||
|
|
||||||
root_dir: PATH
|
root_dir: PATH
|
||||||
@@ -228,6 +235,7 @@ feature -- Handler
|
|||||||
end
|
end
|
||||||
else
|
else
|
||||||
create {FORBIDDEN_ERROR_CMS_RESPONSE} r.make (req, res, api)
|
create {FORBIDDEN_ERROR_CMS_RESPONSE} r.make (req, res, api)
|
||||||
|
r.set_main_content ("You can also contact the webmaster to ask for an account.")
|
||||||
end
|
end
|
||||||
|
|
||||||
r.execute
|
r.execute
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ deferred class
|
|||||||
|
|
||||||
inherit
|
inherit
|
||||||
CMS_CONTENT
|
CMS_CONTENT
|
||||||
|
rename
|
||||||
|
has_identifier as has_id
|
||||||
redefine
|
redefine
|
||||||
debug_output
|
debug_output, has_id
|
||||||
end
|
end
|
||||||
|
|
||||||
REFACTORING_HELPER
|
REFACTORING_HELPER
|
||||||
@@ -63,6 +65,12 @@ feature -- Conversion
|
|||||||
|
|
||||||
feature -- Access
|
feature -- Access
|
||||||
|
|
||||||
|
identifier: detachable IMMUTABLE_STRING_32
|
||||||
|
-- Optional identifier.
|
||||||
|
do
|
||||||
|
create Result.make_from_string_general (id.out)
|
||||||
|
end
|
||||||
|
|
||||||
id: INTEGER_64 assign set_id
|
id: INTEGER_64 assign set_id
|
||||||
-- Unique id.
|
-- Unique id.
|
||||||
--| Should we use NATURAL_64 instead?
|
--| Should we use NATURAL_64 instead?
|
||||||
|
|||||||
@@ -92,246 +92,15 @@ feature -- Forms ...
|
|||||||
|
|
||||||
f.extend (fset)
|
f.extend (fset)
|
||||||
|
|
||||||
-- Path alias
|
-- Path alias
|
||||||
populate_form_with_taxonomy (response, f, a_node)
|
populate_form_with_taxonomy (response, f, a_node)
|
||||||
populate_form_with_path_alias (response, f, a_node)
|
populate_form_with_path_alias (response, f, a_node)
|
||||||
end
|
end
|
||||||
|
|
||||||
populate_form_with_taxonomy (response: NODE_RESPONSE; f: CMS_FORM; a_node: detachable CMS_NODE)
|
populate_form_with_taxonomy (response: CMS_RESPONSE; f: CMS_FORM; a_content: detachable CMS_CONTENT)
|
||||||
local
|
|
||||||
ti: detachable WSF_FORM_TEXT_INPUT
|
|
||||||
th: WSF_FORM_HIDDEN_INPUT
|
|
||||||
w_set: WSF_FORM_FIELD_SET
|
|
||||||
w_select: WSF_FORM_SELECT
|
|
||||||
w_opt: WSF_FORM_SELECT_OPTION
|
|
||||||
w_cb: WSF_FORM_CHECKBOX_INPUT
|
|
||||||
w_voc_set: WSF_FORM_FIELD_SET
|
|
||||||
s: STRING_32
|
|
||||||
voc: CMS_VOCABULARY
|
|
||||||
t: detachable CMS_TERM
|
|
||||||
l_terms: detachable CMS_TERM_COLLECTION
|
|
||||||
l_has_edit_permission: BOOLEAN
|
|
||||||
do
|
do
|
||||||
if
|
if attached {CMS_TAXONOMY_API} response.api.module_api ({CMS_TAXONOMY_MODULE}) as l_taxonomy_api then
|
||||||
attached {CMS_TAXONOMY_API} response.api.module_api ({CMS_TAXONOMY_MODULE}) as l_taxonomy_api and then
|
l_taxonomy_api.populate_edit_form (response, f, content_type.name, a_content)
|
||||||
attached l_taxonomy_api.vocabularies_for_type (content_type.name) as l_vocs and then not l_vocs.is_empty
|
|
||||||
then
|
|
||||||
|
|
||||||
l_has_edit_permission := response.has_permissions (<<"update any taxonomy", "update " + content_type.name + " taxonomy">>)
|
|
||||||
|
|
||||||
-- Handle Taxonomy fields, if any associated with `content_type'.
|
|
||||||
create w_set.make
|
|
||||||
w_set.add_css_class ("taxonomy")
|
|
||||||
l_vocs.sort
|
|
||||||
across
|
|
||||||
l_vocs as vocs_ic
|
|
||||||
loop
|
|
||||||
voc := vocs_ic.item
|
|
||||||
create th.make_with_text ({STRING_32} "taxonomy_vocabularies[" + voc.id.out + "]", voc.name)
|
|
||||||
w_set.extend (th)
|
|
||||||
|
|
||||||
l_terms := Void
|
|
||||||
if a_node /= Void and then a_node.has_id then
|
|
||||||
l_terms := l_taxonomy_api.terms_of_entity (a_node.content_type, a_node.id.out, voc)
|
|
||||||
if l_terms /= Void then
|
|
||||||
l_terms.sort
|
|
||||||
end
|
|
||||||
end
|
|
||||||
create w_voc_set.make
|
|
||||||
w_set.extend (w_voc_set)
|
|
||||||
|
|
||||||
if voc.is_tags then
|
|
||||||
w_voc_set.set_legend (response.translation (voc.name, Void))
|
|
||||||
|
|
||||||
create ti.make ({STRING_32} "taxonomy_" + voc.id.out)
|
|
||||||
w_voc_set.extend (ti)
|
|
||||||
if voc.is_term_required then
|
|
||||||
ti.enable_required
|
|
||||||
end
|
|
||||||
if attached voc.description as l_desc then
|
|
||||||
ti.set_description (response.html_encoded (response.translation (l_desc, Void)))
|
|
||||||
else
|
|
||||||
ti.set_description (response.html_encoded (response.translation (voc.name, Void)))
|
|
||||||
end
|
|
||||||
ti.set_size (70)
|
|
||||||
if l_terms /= Void then
|
|
||||||
create s.make_empty
|
|
||||||
across
|
|
||||||
l_terms as ic
|
|
||||||
loop
|
|
||||||
t := ic.item
|
|
||||||
if not s.is_empty then
|
|
||||||
s.append_character (',')
|
|
||||||
s.append_character (' ')
|
|
||||||
end
|
|
||||||
if ic.item.text.has (' ') then
|
|
||||||
s.append_character ('"')
|
|
||||||
s.append (t.text)
|
|
||||||
s.append_character ('"')
|
|
||||||
else
|
|
||||||
s.append (t.text)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
ti.set_text_value (s)
|
|
||||||
end
|
|
||||||
if not l_has_edit_permission then
|
|
||||||
ti.set_is_readonly (True)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
l_taxonomy_api.fill_vocabularies_with_terms (voc)
|
|
||||||
if not voc.terms.is_empty then
|
|
||||||
if voc.multiple_terms_allowed then
|
|
||||||
if attached voc.description as l_desc then
|
|
||||||
w_voc_set.set_legend (response.html_encoded (l_desc))
|
|
||||||
else
|
|
||||||
w_voc_set.set_legend (response.html_encoded (voc.name))
|
|
||||||
end
|
|
||||||
across
|
|
||||||
voc as voc_terms_ic
|
|
||||||
loop
|
|
||||||
t := voc_terms_ic.item
|
|
||||||
create w_cb.make_with_value ({STRING_32} "taxonomy_" + voc.id.out + "[]", t.text)
|
|
||||||
w_cb.set_title (t.text)
|
|
||||||
w_voc_set.extend (w_cb)
|
|
||||||
if l_terms /= Void and then across l_terms as ic some ic.item.text.same_string (t.text) end then
|
|
||||||
w_cb.set_checked (True)
|
|
||||||
end
|
|
||||||
if not l_has_edit_permission then
|
|
||||||
w_cb.set_is_readonly (True)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
create w_select.make ({STRING_32} "taxonomy_" + voc.id.out)
|
|
||||||
w_voc_set.extend (w_select)
|
|
||||||
|
|
||||||
if attached voc.description as l_desc then
|
|
||||||
w_select.set_description (response.html_encoded (l_desc))
|
|
||||||
else
|
|
||||||
w_select.set_description (response.html_encoded (voc.name))
|
|
||||||
end
|
|
||||||
w_voc_set.set_legend (response.html_encoded (voc.name))
|
|
||||||
|
|
||||||
across
|
|
||||||
voc as voc_terms_ic
|
|
||||||
loop
|
|
||||||
t := voc_terms_ic.item
|
|
||||||
create w_opt.make (response.html_encoded (t.text), response.html_encoded (t.text))
|
|
||||||
w_select.add_option (w_opt)
|
|
||||||
|
|
||||||
if l_terms /= Void and then across l_terms as ic some ic.item.text.same_string (t.text) end then
|
|
||||||
w_opt.set_is_selected (True)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not l_has_edit_permission then
|
|
||||||
w_select.set_is_readonly (True)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
f.submit_actions.extend (agent taxonomy_submit_action (response, l_taxonomy_api, l_vocs, a_node, ?))
|
|
||||||
|
|
||||||
if
|
|
||||||
attached f.fields_by_name ("title") as l_title_fields and then
|
|
||||||
attached l_title_fields.first as l_title_field
|
|
||||||
then
|
|
||||||
f.insert_after (w_set, l_title_field)
|
|
||||||
else
|
|
||||||
f.extend (w_set)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
taxonomy_submit_action (a_response: CMS_RESPONSE; a_taxonomy_api: CMS_TAXONOMY_API; a_vocs: CMS_VOCABULARY_COLLECTION; a_node: detachable CMS_NODE fd: WSF_FORM_DATA)
|
|
||||||
require
|
|
||||||
vocs_not_empty: not a_vocs.is_empty
|
|
||||||
local
|
|
||||||
l_voc_name: READABLE_STRING_32
|
|
||||||
l_terms_to_remove: ARRAYED_LIST [CMS_TERM]
|
|
||||||
l_new_terms: LIST [READABLE_STRING_32]
|
|
||||||
l_text: READABLE_STRING_GENERAL
|
|
||||||
l_found: BOOLEAN
|
|
||||||
t: detachable CMS_TERM
|
|
||||||
vid: INTEGER_64
|
|
||||||
do
|
|
||||||
if
|
|
||||||
a_node /= Void and then a_node.has_id and then
|
|
||||||
attached fd.table_item ("taxonomy_vocabularies") as fd_vocs
|
|
||||||
then
|
|
||||||
if a_response.has_permissions (<<{STRING_32} "update any taxonomy", {STRING_32} "update " + content_type.name + " taxonomy">>) then
|
|
||||||
across
|
|
||||||
fd_vocs.values as ic
|
|
||||||
loop
|
|
||||||
vid := ic.key.to_integer_64
|
|
||||||
l_voc_name := ic.item.string_representation
|
|
||||||
|
|
||||||
if attached a_vocs.item_by_id (vid) as voc then
|
|
||||||
if attached fd.string_item ("taxonomy_" + vid.out) as l_string then
|
|
||||||
l_new_terms := a_taxonomy_api.splitted_string (l_string, ',')
|
|
||||||
elseif attached fd.table_item ("taxonomy_" + vid.out) as fd_terms then
|
|
||||||
create {ARRAYED_LIST [READABLE_STRING_32]} l_new_terms.make (fd_terms.count)
|
|
||||||
across
|
|
||||||
fd_terms as t_ic
|
|
||||||
loop
|
|
||||||
l_new_terms.force (t_ic.item.string_representation)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
create {ARRAYED_LIST [READABLE_STRING_32]} l_new_terms.make (0)
|
|
||||||
end
|
|
||||||
|
|
||||||
create l_terms_to_remove.make (0)
|
|
||||||
if attached a_taxonomy_api.terms_of_entity (content_type.name, a_node.id.out, voc) as l_existing_terms then
|
|
||||||
across
|
|
||||||
l_existing_terms as t_ic
|
|
||||||
loop
|
|
||||||
l_text := t_ic.item.text
|
|
||||||
from
|
|
||||||
l_found := False
|
|
||||||
l_new_terms.start
|
|
||||||
until
|
|
||||||
l_new_terms.after
|
|
||||||
loop
|
|
||||||
if l_new_terms.item.same_string_general (l_text) then
|
|
||||||
-- Already associated with term `t_ic.text'.
|
|
||||||
l_found := True
|
|
||||||
l_new_terms.remove
|
|
||||||
else
|
|
||||||
l_new_terms.forth
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not l_found then
|
|
||||||
-- Remove term
|
|
||||||
l_terms_to_remove.force (t_ic.item)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
across
|
|
||||||
l_terms_to_remove as t_ic
|
|
||||||
loop
|
|
||||||
a_taxonomy_api.unassociate_term_from_entity (t_ic.item, content_type.name, a_node.id.out)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
across
|
|
||||||
l_new_terms as t_ic
|
|
||||||
loop
|
|
||||||
t := a_taxonomy_api.term_by_text (t_ic.item, voc)
|
|
||||||
if
|
|
||||||
t = Void and voc.is_tags
|
|
||||||
then
|
|
||||||
-- Create new term!
|
|
||||||
create t.make (t_ic.item)
|
|
||||||
a_taxonomy_api.save_term (t, voc)
|
|
||||||
if a_taxonomy_api.has_error then
|
|
||||||
t := Void
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if t /= Void then
|
|
||||||
a_taxonomy_api.associate_term_with_entity (t, content_type.name, a_node.id.out)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -565,30 +334,9 @@ feature -- Output
|
|||||||
|
|
||||||
if
|
if
|
||||||
a_response /= Void and then
|
a_response /= Void and then
|
||||||
attached {CMS_TAXONOMY_API} cms_api.module_api ({CMS_TAXONOMY_MODULE}) as l_taxonomy_api and then
|
attached {CMS_TAXONOMY_API} cms_api.module_api ({CMS_TAXONOMY_MODULE}) as l_taxonomy_api
|
||||||
attached l_taxonomy_api.vocabularies_for_type (content_type.name) as vocs and then not vocs.is_empty
|
|
||||||
then
|
then
|
||||||
vocs.sort
|
l_taxonomy_api.append_taxonomy_to_xhtml (a_node, a_response, a_output)
|
||||||
across
|
|
||||||
vocs as ic
|
|
||||||
loop
|
|
||||||
if
|
|
||||||
attached l_taxonomy_api.terms_of_entity (content_type.name, a_node.id.out, ic.item) as l_terms and then
|
|
||||||
not l_terms.is_empty
|
|
||||||
then
|
|
||||||
a_output.append ("<ul class=%"taxonomy term-" + ic.item.id.out + "%">")
|
|
||||||
a_output.append (l_node_api.html_encoded (ic.item.name))
|
|
||||||
a_output.append (": ")
|
|
||||||
across
|
|
||||||
l_terms as t_ic
|
|
||||||
loop
|
|
||||||
a_output.append ("<li>")
|
|
||||||
a_response.append_link_to_html (t_ic.item.text, "taxonomy/term/" + t_ic.item.id.out, Void, a_output)
|
|
||||||
a_output.append ("</li>")
|
|
||||||
end
|
|
||||||
a_output.append ("</ul>%N")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- We don't show the summary on the detail page, since its just a short view of the full content. Otherwise we would write the same thing twice.
|
-- We don't show the summary on the detail page, since its just a short view of the full content. Otherwise we would write the same thing twice.
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ feature {CMS_API} -- Module management
|
|||||||
l_sql_storage.sql_execute_file_script (api.module_resource_location (Current, (create {PATH}.make_from_string ("scripts")).extended ("oauth2_consumers.sql")), Void)
|
l_sql_storage.sql_execute_file_script (api.module_resource_location (Current, (create {PATH}.make_from_string ("scripts")).extended ("oauth2_consumers.sql")), Void)
|
||||||
|
|
||||||
if l_sql_storage.has_error then
|
if l_sql_storage.has_error then
|
||||||
api.logger.put_error ("Could not initialize database for blog module", generating_type)
|
api.logger.put_error ("Could not initialize database for oauth_20 module", generating_type)
|
||||||
end
|
end
|
||||||
-- TODO workaround.
|
-- TODO workaround.
|
||||||
l_sql_storage.sql_execute_file_script (api.module_resource_location (Current, (create {PATH}.make_from_string ("scripts")).extended ("oauth2_consumers_initialize.sql")), Void)
|
l_sql_storage.sql_execute_file_script (api.module_resource_location (Current, (create {PATH}.make_from_string ("scripts")).extended ("oauth2_consumers_initialize.sql")), Void)
|
||||||
@@ -108,7 +108,7 @@ feature {CMS_API} -- Module management
|
|||||||
-- TODO workaround, until we have an admin module
|
-- TODO workaround, until we have an admin module
|
||||||
l_sql_storage.sql_query ("SELECT name FROM oauth2_consumers;", Void)
|
l_sql_storage.sql_query ("SELECT name FROM oauth2_consumers;", Void)
|
||||||
if l_sql_storage.has_error then
|
if l_sql_storage.has_error then
|
||||||
api.logger.put_error ("Could not initialize database for differnent consumerns", generating_type)
|
api.logger.put_error ("Could not initialize database for differnent consumers", generating_type)
|
||||||
else
|
else
|
||||||
from
|
from
|
||||||
l_sql_storage.sql_start
|
l_sql_storage.sql_start
|
||||||
@@ -227,7 +227,10 @@ feature -- Hooks
|
|||||||
until
|
until
|
||||||
lnk2 /= Void
|
lnk2 /= Void
|
||||||
loop
|
loop
|
||||||
if ic.item.location.same_string ("account/roc-logout") then
|
if
|
||||||
|
ic.item.location.same_string ("account/roc-logout") or else
|
||||||
|
ic.item.location.same_string ("basic_auth_logoff")
|
||||||
|
then
|
||||||
lnk2 := ic.item
|
lnk2 := ic.item
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -203,7 +203,10 @@ feature -- Hooks
|
|||||||
until
|
until
|
||||||
lnk2 /= Void
|
lnk2 /= Void
|
||||||
loop
|
loop
|
||||||
if ic.item.location.same_string ("account/roc-logout") then
|
if
|
||||||
|
ic.item.location.same_string ("account/roc-logout") or else
|
||||||
|
ic.item.location.same_string ("basic_auth_logoff")
|
||||||
|
then
|
||||||
lnk2 := ic.item
|
lnk2 := ic.item
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ feature {NONE} -- User OpenID
|
|||||||
|
|
||||||
Sql_insert_openid: STRING = "INSERT INTO openid_items (uid, identity, created) VALUES (:uid, :identity, :utc_date);"
|
Sql_insert_openid: STRING = "INSERT INTO openid_items (uid, identity, created) VALUES (:uid, :identity, :utc_date);"
|
||||||
|
|
||||||
Sql_openid_consumers: STRING = "SELECT name FROM openid_consumers";
|
Sql_openid_consumers: STRING = "SELECT name FROM openid_consumers;"
|
||||||
|
|
||||||
|
|
||||||
feature {NONE} -- Consumer
|
feature {NONE} -- Consumer
|
||||||
|
|||||||
63
modules/session_auth/cms_session_api.e
Normal file
63
modules/session_auth/cms_session_api.e
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
note
|
||||||
|
description: "API to manage CMS User session authentication"
|
||||||
|
date: "$Date$"
|
||||||
|
revision: "$Revision$"
|
||||||
|
|
||||||
|
class
|
||||||
|
CMS_SESSION_API
|
||||||
|
|
||||||
|
|
||||||
|
inherit
|
||||||
|
CMS_MODULE_API
|
||||||
|
|
||||||
|
REFACTORING_HELPER
|
||||||
|
|
||||||
|
create {CMS_SESSION_AUTH_MODULE}
|
||||||
|
make_with_storage
|
||||||
|
|
||||||
|
feature {NONE} -- Initialization
|
||||||
|
|
||||||
|
make_with_storage (a_api: CMS_API; a_session_auth_storage: CMS_SESSION_AUTH_STORAGE_I)
|
||||||
|
-- Create an object with api `a_api' and storage `a_session_auth_storage'.
|
||||||
|
do
|
||||||
|
session_auth_storage := a_session_auth_storage
|
||||||
|
make (a_api)
|
||||||
|
ensure
|
||||||
|
session_auth_storage_set: session_auth_storage = a_session_auth_storage
|
||||||
|
end
|
||||||
|
|
||||||
|
feature {CMS_MODULE} -- Access: User session storage.
|
||||||
|
|
||||||
|
session_auth_storage: CMS_SESSION_AUTH_STORAGE_I
|
||||||
|
-- storage interface.
|
||||||
|
|
||||||
|
feature -- Access
|
||||||
|
|
||||||
|
user_by_session_token (a_token: READABLE_STRING_32): detachable CMS_USER
|
||||||
|
-- Retrieve user by token `a_token', if any.
|
||||||
|
do
|
||||||
|
Result := session_auth_storage.user_by_session_token (a_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
has_user_token (a_user: CMS_USER): BOOLEAN
|
||||||
|
-- Has the user `a_user' and associated session token?
|
||||||
|
do
|
||||||
|
Result := session_auth_storage.has_user_token (a_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
feature -- Change User session
|
||||||
|
|
||||||
|
new_user_session_auth (a_token: READABLE_STRING_GENERAL; a_user: CMS_USER;)
|
||||||
|
-- New user session for user `a_user' with token `a_token'.
|
||||||
|
do
|
||||||
|
session_auth_storage.new_user_session_auth (a_token, a_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
update_user_session_auth (a_token: READABLE_STRING_GENERAL; a_user: CMS_USER )
|
||||||
|
-- Update user session for user `a_user' with token `a_token'.
|
||||||
|
do
|
||||||
|
session_auth_storage.update_user_session_auth (a_token, a_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
28
modules/session_auth/cms_session_auth-safe.ecf
Normal file
28
modules/session_auth/cms_session_auth-safe.ecf
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||||
|
<system xmlns="http://www.eiffel.com/developers/xml/configuration-1-15-0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.eiffel.com/developers/xml/configuration-1-15-0 http://www.eiffel.com/developers/xml/configuration-1-15-0.xsd" name="session_auth" uuid="8A43B6DD-6B39-472C-9A96-978414CBF1E3" library_target="session_auth">
|
||||||
|
<target name="session_auth">
|
||||||
|
<root all_classes="true"/>
|
||||||
|
<file_rule>
|
||||||
|
<exclude>/EIFGENs$</exclude>
|
||||||
|
<exclude>/CVS$</exclude>
|
||||||
|
<exclude>/.svn$</exclude>
|
||||||
|
</file_rule>
|
||||||
|
<option warning="true" full_class_checking="false" is_attached_by_default="true" is_obsolete_routine_type="true" void_safety="all" syntax="transitional">
|
||||||
|
<assertions precondition="true" postcondition="true" check="true" invariant="true" loop="true" supplier_precondition="true"/>
|
||||||
|
</option>
|
||||||
|
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/>
|
||||||
|
<library name="cms" location="..\..\cms-safe.ecf"/>
|
||||||
|
<library name="cms_app_env" location="..\..\library\app_env\app_env-safe.ecf" readonly="false"/>
|
||||||
|
<library name="cms_auth_module" location="..\..\modules\auth\auth-safe.ecf" readonly="false"/>
|
||||||
|
<library name="cms_model" location="..\..\library\model\cms_model-safe.ecf" readonly="false"/>
|
||||||
|
<library name="crypto" location="$ISE_LIBRARY\unstable\library\text\encryption\crypto\crypto-safe.ecf"/>
|
||||||
|
<library name="encoder" location="$ISE_LIBRARY\contrib\library\web\framework\ewf\text\encoder\encoder-safe.ecf"/>
|
||||||
|
<library name="error" location="$ISE_LIBRARY\contrib\library\utility\general\error\error-safe.ecf"/>
|
||||||
|
<library name="http" location="$ISE_LIBRARY\contrib\library\network\protocol\http\http-safe.ecf"/>
|
||||||
|
<library name="http_authorization" location="$ISE_LIBRARY\contrib\library\web\authentication\http_authorization\http_authorization-safe.ecf" readonly="false"/>
|
||||||
|
<library name="time" location="$ISE_LIBRARY\library\time\time-safe.ecf"/>
|
||||||
|
<library name="wsf" location="$ISE_LIBRARY\contrib\library\web\framework\ewf\wsf\wsf-safe.ecf"/>
|
||||||
|
<library name="wsf_extension" location="$ISE_LIBRARY\contrib\library\web\framework\ewf\wsf\wsf_extension-safe.ecf" readonly="false"/>
|
||||||
|
<cluster name="src" location=".\" recursive="true"/>
|
||||||
|
</target>
|
||||||
|
</system>
|
||||||
349
modules/session_auth/cms_session_auth_module.e
Normal file
349
modules/session_auth/cms_session_auth_module.e
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
note
|
||||||
|
description: "[
|
||||||
|
This module allows the use Session Based Authentication using Cookies to restrict access
|
||||||
|
by looking up users in the given providers.
|
||||||
|
]"
|
||||||
|
date: "$Date$"
|
||||||
|
revision: "$Revision$"
|
||||||
|
|
||||||
|
class
|
||||||
|
CMS_SESSION_AUTH_MODULE
|
||||||
|
|
||||||
|
inherit
|
||||||
|
CMS_MODULE
|
||||||
|
rename
|
||||||
|
module_api as user_session_api
|
||||||
|
redefine
|
||||||
|
filters,
|
||||||
|
setup_hooks,
|
||||||
|
initialize,
|
||||||
|
install,
|
||||||
|
user_session_api
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
CMS_HOOK_AUTO_REGISTER
|
||||||
|
|
||||||
|
CMS_HOOK_BLOCK
|
||||||
|
|
||||||
|
CMS_HOOK_MENU_SYSTEM_ALTER
|
||||||
|
|
||||||
|
CMS_HOOK_VALUE_TABLE_ALTER
|
||||||
|
|
||||||
|
SHARED_LOGGER
|
||||||
|
|
||||||
|
CMS_REQUEST_UTIL
|
||||||
|
|
||||||
|
create
|
||||||
|
make
|
||||||
|
|
||||||
|
feature {NONE} -- Initialization
|
||||||
|
|
||||||
|
make
|
||||||
|
do
|
||||||
|
version := "1.0"
|
||||||
|
description := "Service to manage cookie based authentication"
|
||||||
|
package := "authentication"
|
||||||
|
add_dependency ({CMS_AUTHENTICATION_MODULE})
|
||||||
|
end
|
||||||
|
|
||||||
|
feature -- Access
|
||||||
|
|
||||||
|
name: STRING = "session_auth"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
feature {CMS_API} -- Module Initialization
|
||||||
|
|
||||||
|
initialize (a_api: CMS_API)
|
||||||
|
-- <Precursor>
|
||||||
|
local
|
||||||
|
l_session_auth_api: like user_session_api
|
||||||
|
l_user_auth_storage: CMS_SESSION_AUTH_STORAGE_I
|
||||||
|
do
|
||||||
|
Precursor (a_api)
|
||||||
|
|
||||||
|
-- Storage initialization
|
||||||
|
if attached a_api.storage.as_sql_storage as l_storage_sql then
|
||||||
|
create {CMS_SESSION_AUTH_STORAGE_SQL} l_user_auth_storage.make (l_storage_sql)
|
||||||
|
else
|
||||||
|
-- FIXME: in case of NULL storage, should Current be disabled?
|
||||||
|
create {CMS_SESSION_AUTH_STORAGE_NULL} l_user_auth_storage
|
||||||
|
end
|
||||||
|
|
||||||
|
-- API initialization
|
||||||
|
create l_session_auth_api.make_with_storage (a_api, l_user_auth_storage)
|
||||||
|
user_session_api := l_session_auth_api
|
||||||
|
ensure then
|
||||||
|
session_auth_api_set: user_session_api /= Void
|
||||||
|
end
|
||||||
|
|
||||||
|
feature {CMS_API} -- Module management
|
||||||
|
|
||||||
|
install (api: CMS_API)
|
||||||
|
do
|
||||||
|
-- Schema
|
||||||
|
if attached api.storage.as_sql_storage as l_sql_storage then
|
||||||
|
if not l_sql_storage.sql_table_exists ("session_auth") then
|
||||||
|
--| Schema
|
||||||
|
l_sql_storage.sql_execute_file_script (api.module_resource_location (Current, (create {PATH}.make_from_string ("scripts")).extended ("session_auth_table.sql")), Void)
|
||||||
|
|
||||||
|
if l_sql_storage.has_error then
|
||||||
|
api.logger.put_error ("Could not initialize database for session auth module", generating_type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
l_sql_storage.sql_finalize
|
||||||
|
Precursor {CMS_MODULE}(api)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
feature {CMS_API} -- Access: API
|
||||||
|
|
||||||
|
user_session_api: detachable CMS_SESSION_API
|
||||||
|
-- <Precursor>
|
||||||
|
|
||||||
|
feature -- Access: router
|
||||||
|
|
||||||
|
setup_router (a_router: WSF_ROUTER; a_api: CMS_API)
|
||||||
|
-- <Precursor>
|
||||||
|
do
|
||||||
|
a_router.handle ("/account/roc-session-login", create {WSF_URI_AGENT_HANDLER}.make (agent handle_login(a_api, ?, ?)), a_router.methods_head_get)
|
||||||
|
a_router.handle ("/account/roc-session-logout", create {WSF_URI_AGENT_HANDLER}.make (agent handle_logout (a_api, ?, ?)), a_router.methods_get_post)
|
||||||
|
a_router.handle ("/account/login-with-session", create {WSF_URI_TEMPLATE_AGENT_HANDLER}.make (agent handle_login_with_session (a_api,user_session_api, ?, ?)), a_router.methods_get_post)
|
||||||
|
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)
|
||||||
|
if attached user_session_api as l_session_api then
|
||||||
|
Result.extend (create {CMS_SESSION_AUTH_FILTER}.make (a_api, l_session_api))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
feature {NONE} -- Implementation: routes
|
||||||
|
|
||||||
|
handle_login (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||||
|
local
|
||||||
|
r: CMS_RESPONSE
|
||||||
|
do
|
||||||
|
create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
|
||||||
|
r.execute
|
||||||
|
end
|
||||||
|
|
||||||
|
handle_logout (api: CMS_API; req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||||
|
local
|
||||||
|
r: CMS_RESPONSE
|
||||||
|
l_cookie: WSF_COOKIE
|
||||||
|
do
|
||||||
|
if
|
||||||
|
attached {WSF_STRING} req.cookie ({CMS_SESSION_CONSTANTS}.session_auth_token) as l_cookie_token and then
|
||||||
|
attached {CMS_USER} current_user (req) as l_user
|
||||||
|
then
|
||||||
|
-- Logout Session
|
||||||
|
create l_cookie.make ({CMS_SESSION_CONSTANTS}.session_auth_token, l_cookie_token.value)
|
||||||
|
l_cookie.set_path ("/")
|
||||||
|
l_cookie.set_max_age (-1)
|
||||||
|
res.add_cookie (l_cookie)
|
||||||
|
unset_current_user (req)
|
||||||
|
|
||||||
|
create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
|
||||||
|
r.set_status_code ({HTTP_CONSTANTS}.found)
|
||||||
|
r.set_redirection (req.absolute_script_url (""))
|
||||||
|
r.execute
|
||||||
|
else
|
||||||
|
fixme (generator + ": missing else implementation in handle_logout!")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
handle_login_with_session (api: CMS_API; a_session_api: detachable CMS_SESSION_API; req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||||
|
local
|
||||||
|
r: CMS_RESPONSE
|
||||||
|
l_token: STRING
|
||||||
|
l_cookie: WSF_COOKIE
|
||||||
|
do
|
||||||
|
if
|
||||||
|
attached a_session_api as l_session_api and then
|
||||||
|
attached {WSF_STRING} req.form_parameter ("username") as l_username and then
|
||||||
|
attached {WSF_STRING} req.form_parameter ("password") as l_password and then
|
||||||
|
api.user_api.is_valid_credential (l_username.value, l_password.value) and then
|
||||||
|
attached api.user_api.user_by_name (l_username.value) as l_user
|
||||||
|
then
|
||||||
|
l_token := generate_token
|
||||||
|
if
|
||||||
|
a_session_api.has_user_token (l_user)
|
||||||
|
then
|
||||||
|
l_session_api.update_user_session_auth (l_token, l_user)
|
||||||
|
else
|
||||||
|
l_session_api.new_user_session_auth (l_token, l_user)
|
||||||
|
end
|
||||||
|
create l_cookie.make ({CMS_SESSION_CONSTANTS}.session_auth_token, l_token)
|
||||||
|
l_cookie.set_max_age ({CMS_SESSION_CONSTANTS}.session_max_age)
|
||||||
|
l_cookie.set_path ("/")
|
||||||
|
res.add_cookie (l_cookie)
|
||||||
|
set_current_user (req, l_user)
|
||||||
|
create {GENERIC_VIEW_CMS_RESPONSE} r.make (req, res, api)
|
||||||
|
r.set_redirection (req.absolute_script_url (""))
|
||||||
|
r.execute
|
||||||
|
else
|
||||||
|
create {BAD_REQUEST_ERROR_CMS_RESPONSE} r.make (req, res, api)
|
||||||
|
if attached template_block ("login", r) as l_tpl_block then
|
||||||
|
if attached {WSF_STRING} req.form_parameter ("username") as l_username then
|
||||||
|
l_tpl_block.set_value (l_username.value, "username")
|
||||||
|
end
|
||||||
|
l_tpl_block.set_value ("Wrong: Username or password ", "error")
|
||||||
|
r.add_block (l_tpl_block, "content")
|
||||||
|
end
|
||||||
|
r.execute
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
feature -- Hooks configuration
|
||||||
|
|
||||||
|
setup_hooks (a_hooks: CMS_HOOK_CORE_MANAGER)
|
||||||
|
-- Module hooks configuration.
|
||||||
|
do
|
||||||
|
auto_subscribe_to_hooks (a_hooks)
|
||||||
|
a_hooks.subscribe_to_block_hook (Current)
|
||||||
|
a_hooks.subscribe_to_value_table_alter_hook (Current)
|
||||||
|
end
|
||||||
|
|
||||||
|
feature -- Hooks
|
||||||
|
|
||||||
|
value_table_alter (a_value: CMS_VALUE_TABLE; a_response: CMS_RESPONSE)
|
||||||
|
-- <Precursor>
|
||||||
|
do
|
||||||
|
if
|
||||||
|
attached a_response.user as u and then
|
||||||
|
attached {WSF_STRING} a_response.request.cookie ({CMS_SESSION_CONSTANTS}.session_auth_token)
|
||||||
|
then
|
||||||
|
a_value.force ("account/roc-session-logout", "auth_login_strategy")
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
menu_system_alter (a_menu_system: CMS_MENU_SYSTEM; a_response: CMS_RESPONSE)
|
||||||
|
-- Hook execution on collection of menu contained by `a_menu_system'
|
||||||
|
-- for related response `a_response'.
|
||||||
|
local
|
||||||
|
lnk: CMS_LOCAL_LINK
|
||||||
|
lnk2: detachable CMS_LINK
|
||||||
|
do
|
||||||
|
if
|
||||||
|
attached a_response.user as u and then
|
||||||
|
attached {WSF_STRING} a_response.request.cookie ({CMS_SESSION_CONSTANTS}.session_auth_token)
|
||||||
|
then
|
||||||
|
across
|
||||||
|
a_menu_system.primary_menu.items as ic
|
||||||
|
until
|
||||||
|
lnk2 /= Void
|
||||||
|
loop
|
||||||
|
if ic.item.location.same_string ("account/roc-logout") or else ic.item.location.same_string ("basic_auth_logoff") then
|
||||||
|
lnk2 := ic.item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if lnk2 /= Void then
|
||||||
|
a_menu_system.primary_menu.remove (lnk2)
|
||||||
|
end
|
||||||
|
create lnk.make ("Logout", "account/roc-session-logout" )
|
||||||
|
a_menu_system.primary_menu.extend (lnk)
|
||||||
|
else
|
||||||
|
if a_response.location.starts_with ("account/") then
|
||||||
|
create lnk.make ("Session", "account/roc-session-login")
|
||||||
|
a_response.add_to_primary_tabs (lnk)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
block_list: ITERABLE [like {CMS_BLOCK}.name]
|
||||||
|
local
|
||||||
|
l_string: STRING
|
||||||
|
do
|
||||||
|
Result := <<"login">>
|
||||||
|
debug ("roc")
|
||||||
|
create l_string.make_empty
|
||||||
|
across
|
||||||
|
Result as ic
|
||||||
|
loop
|
||||||
|
l_string.append (ic.item)
|
||||||
|
l_string.append_character (' ')
|
||||||
|
end
|
||||||
|
write_debug_log (generator + ".block_list:" + l_string )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get_block_view (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
|
||||||
|
do
|
||||||
|
if
|
||||||
|
a_block_id.is_case_insensitive_equal_general ("login") and then
|
||||||
|
a_response.location.starts_with ("account/roc-session-login")
|
||||||
|
then
|
||||||
|
get_block_view_login (a_block_id, a_response)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
feature {NONE} -- Helpers
|
||||||
|
|
||||||
|
template_block (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE): detachable CMS_SMARTY_TEMPLATE_BLOCK
|
||||||
|
-- Smarty content block for `a_block_id'
|
||||||
|
local
|
||||||
|
p: detachable PATH
|
||||||
|
do
|
||||||
|
create p.make_from_string ("templates")
|
||||||
|
p := p.extended ("block_").appended (a_block_id).appended_with_extension ("tpl")
|
||||||
|
|
||||||
|
p := a_response.api.module_theme_resource_location (Current, p)
|
||||||
|
if p /= Void then
|
||||||
|
if attached p.entry as e then
|
||||||
|
create Result.make (a_block_id, Void, p.parent, e)
|
||||||
|
else
|
||||||
|
create Result.make (a_block_id, Void, p.parent, p)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
feature {NONE} -- Block views
|
||||||
|
|
||||||
|
get_block_view_login (a_block_id: READABLE_STRING_8; a_response: CMS_RESPONSE)
|
||||||
|
local
|
||||||
|
vals: CMS_VALUE_TABLE
|
||||||
|
do
|
||||||
|
if attached template_block (a_block_id, a_response) as l_tpl_block then
|
||||||
|
create vals.make (1)
|
||||||
|
-- add the variable to the block
|
||||||
|
value_table_alter (vals, a_response)
|
||||||
|
across
|
||||||
|
vals as ic
|
||||||
|
loop
|
||||||
|
l_tpl_block.set_value (ic.item, ic.key)
|
||||||
|
end
|
||||||
|
a_response.add_block (l_tpl_block, "content")
|
||||||
|
else
|
||||||
|
debug ("cms")
|
||||||
|
a_response.add_warning_message ("Error with block [" + a_block_id + "]")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
generate_token: STRING
|
||||||
|
-- Generate token to use in a Session.
|
||||||
|
local
|
||||||
|
l_token: STRING
|
||||||
|
l_security: CMS_TOKEN_GENERATOR
|
||||||
|
l_encode: URL_ENCODER
|
||||||
|
do
|
||||||
|
create l_security
|
||||||
|
l_token := l_security.token
|
||||||
|
create l_encode
|
||||||
|
from until l_token.same_string (l_encode.encoded_string (l_token)) loop
|
||||||
|
-- Loop ensure that we have a security token that does not contain characters that need encoding.
|
||||||
|
-- We cannot simply to an encode-decode because the email sent to the user will contain an encoded token
|
||||||
|
-- but the user will need to use an unencoded token if activation has to be done manually.
|
||||||
|
l_token := l_security.token
|
||||||
|
end
|
||||||
|
Result := l_token
|
||||||
|
end
|
||||||
|
end
|
||||||
19
modules/session_auth/cms_session_constants.e
Normal file
19
modules/session_auth/cms_session_constants.e
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
note
|
||||||
|
description: "Summary description for {CMS_SESSION_CONSTANTS}."
|
||||||
|
date: "$Date$"
|
||||||
|
revision: "$Revision$"
|
||||||
|
|
||||||
|
class
|
||||||
|
CMS_SESSION_CONSTANTS
|
||||||
|
|
||||||
|
|
||||||
|
feature
|
||||||
|
session_auth_token: STRING = "EWF_ROC_SESSION_AUTH_TOKEN_"
|
||||||
|
-- Name of Cookie used to keep the session info.
|
||||||
|
-- TODO add a config file to be able to customize this value via coniguration file.
|
||||||
|
|
||||||
|
session_max_age: INTEGER = 86400
|
||||||
|
-- Value of the Max-Age, before the cookie expires.
|
||||||
|
-- TODO add a config file to be able to customize this value via coniguration file.
|
||||||
|
|
||||||
|
end
|
||||||
55
modules/session_auth/filter/cms_session_auth_filter.e
Normal file
55
modules/session_auth/filter/cms_session_auth_filter.e
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
note
|
||||||
|
description: "[
|
||||||
|
Processes a HTTP request's checking Session cookies, putting the result into the execution variable user.
|
||||||
|
]"
|
||||||
|
date: "$Date: 2015-02-13 13:08:13 +0100 (ven., 13 févr. 2015) $"
|
||||||
|
revision: "$Revision: 96616 $"
|
||||||
|
|
||||||
|
class
|
||||||
|
CMS_SESSION_AUTH_FILTER
|
||||||
|
|
||||||
|
inherit
|
||||||
|
WSF_URI_TEMPLATE_HANDLER
|
||||||
|
|
||||||
|
CMS_HANDLER
|
||||||
|
rename
|
||||||
|
make as make_handler
|
||||||
|
end
|
||||||
|
|
||||||
|
WSF_FILTER
|
||||||
|
|
||||||
|
create
|
||||||
|
make
|
||||||
|
|
||||||
|
feature {NONE} -- Initialization
|
||||||
|
|
||||||
|
make (a_api: CMS_API; a_session_oauth_api: CMS_SESSION_API)
|
||||||
|
do
|
||||||
|
make_handler (a_api)
|
||||||
|
session_oauth_api := a_session_oauth_api
|
||||||
|
end
|
||||||
|
|
||||||
|
session_oauth_api: CMS_SESSION_API
|
||||||
|
|
||||||
|
feature -- Basic operations
|
||||||
|
|
||||||
|
execute (req: WSF_REQUEST; res: WSF_RESPONSE)
|
||||||
|
-- Execute the filter.
|
||||||
|
do
|
||||||
|
api.logger.put_debug (generator + ".execute ", Void)
|
||||||
|
-- A valid user
|
||||||
|
if
|
||||||
|
attached {WSF_STRING} req.cookie ({CMS_SESSION_CONSTANTS}.session_auth_token) as l_roc_auth_session_token
|
||||||
|
then
|
||||||
|
if attached session_oauth_api.user_by_session_token (l_roc_auth_session_token.value) as l_user then
|
||||||
|
set_current_user (req, l_user)
|
||||||
|
else
|
||||||
|
api.logger.put_error (generator + ".execute login_valid failed for: " + l_roc_auth_session_token.value , Void)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
api.logger.put_debug (generator + ".execute without authentication", Void)
|
||||||
|
end
|
||||||
|
execute_next (req, res)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
note
|
||||||
|
description: "[
|
||||||
|
API to handle OAUTH storage
|
||||||
|
]"
|
||||||
|
date: "$Date$"
|
||||||
|
revision: "$Revision$"
|
||||||
|
|
||||||
|
deferred class
|
||||||
|
CMS_SESSION_AUTH_STORAGE_I
|
||||||
|
|
||||||
|
inherit
|
||||||
|
SHARED_LOGGER
|
||||||
|
|
||||||
|
feature -- Error Handling
|
||||||
|
|
||||||
|
error_handler: ERROR_HANDLER
|
||||||
|
-- Error handler.
|
||||||
|
deferred
|
||||||
|
end
|
||||||
|
|
||||||
|
feature -- Access: Users
|
||||||
|
|
||||||
|
user_by_session_token (a_token: READABLE_STRING_32): detachable CMS_USER
|
||||||
|
-- Retrieve user by token `a_token', if any.
|
||||||
|
deferred
|
||||||
|
end
|
||||||
|
|
||||||
|
has_user_token (a_user: CMS_USER): BOOLEAN
|
||||||
|
-- Has the user `a_user' and associated session token?
|
||||||
|
deferred
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
feature -- Change User session
|
||||||
|
|
||||||
|
new_user_session_auth (a_token: READABLE_STRING_GENERAL; a_user: CMS_USER;)
|
||||||
|
-- New user session for user `a_user' with token `a_token'.
|
||||||
|
deferred
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
update_user_session_auth (a_token: READABLE_STRING_GENERAL; a_user: CMS_USER )
|
||||||
|
-- Update user session for user `a_user' with token `a_token'.
|
||||||
|
deferred
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
note
|
||||||
|
description: "Summary description for {CMS_SESSION_AUTH_STORAGE_NULL}."
|
||||||
|
date: "$Date$"
|
||||||
|
revision: "$Revision$"
|
||||||
|
|
||||||
|
class
|
||||||
|
CMS_SESSION_AUTH_STORAGE_NULL
|
||||||
|
|
||||||
|
inherit
|
||||||
|
|
||||||
|
CMS_SESSION_AUTH_STORAGE_I
|
||||||
|
|
||||||
|
|
||||||
|
feature -- Error handler
|
||||||
|
|
||||||
|
error_handler: ERROR_HANDLER
|
||||||
|
-- Error handler.
|
||||||
|
do
|
||||||
|
create Result.make
|
||||||
|
end
|
||||||
|
|
||||||
|
feature -- Access
|
||||||
|
|
||||||
|
user_by_session_token (a_token: READABLE_STRING_32): detachable CMS_USER
|
||||||
|
-- Retrieve user by token `a_token', if any.
|
||||||
|
do
|
||||||
|
end
|
||||||
|
|
||||||
|
has_user_token (a_user: CMS_USER): BOOLEAN
|
||||||
|
-- Has the user `a_user' and associated session token?
|
||||||
|
do
|
||||||
|
end
|
||||||
|
|
||||||
|
feature -- Change User session
|
||||||
|
|
||||||
|
new_user_session_auth (a_token: READABLE_STRING_GENERAL; a_user: CMS_USER;)
|
||||||
|
-- New user session for user `a_user' with token `a_token'.
|
||||||
|
do
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
update_user_session_auth (a_token: READABLE_STRING_GENERAL; a_user: CMS_USER )
|
||||||
|
-- Update user session for user `a_user' with token `a_token'.
|
||||||
|
do
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
155
modules/session_auth/persistence/cms_session_auth_storage_sql.e
Normal file
155
modules/session_auth/persistence/cms_session_auth_storage_sql.e
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
note
|
||||||
|
description: "Summary description for {CMS_SESSION_AUTH_STORAGE_SQL}."
|
||||||
|
date: "$Date$"
|
||||||
|
revision: "$Revision$"
|
||||||
|
|
||||||
|
class
|
||||||
|
CMS_SESSION_AUTH_STORAGE_SQL
|
||||||
|
|
||||||
|
inherit
|
||||||
|
|
||||||
|
CMS_SESSION_AUTH_STORAGE_I
|
||||||
|
|
||||||
|
CMS_PROXY_STORAGE_SQL
|
||||||
|
|
||||||
|
CMS_SESSION_AUTH_STORAGE_I
|
||||||
|
|
||||||
|
CMS_STORAGE_SQL_I
|
||||||
|
|
||||||
|
REFACTORING_HELPER
|
||||||
|
|
||||||
|
create
|
||||||
|
make
|
||||||
|
|
||||||
|
feature -- Access User
|
||||||
|
|
||||||
|
user_by_session_token (a_token: READABLE_STRING_32): detachable CMS_USER
|
||||||
|
-- Retrieve user by token `a_token', if any.
|
||||||
|
local
|
||||||
|
l_parameters: STRING_TABLE [detachable ANY]
|
||||||
|
do
|
||||||
|
error_handler.reset
|
||||||
|
write_information_log (generator + ".user_by_session_token")
|
||||||
|
create l_parameters.make (1)
|
||||||
|
l_parameters.put (a_token, "token")
|
||||||
|
sql_query (Select_user_by_token, l_parameters)
|
||||||
|
if not has_error and not sql_after then
|
||||||
|
Result := fetch_user
|
||||||
|
sql_forth
|
||||||
|
if not sql_after then
|
||||||
|
check
|
||||||
|
no_more_than_one: False
|
||||||
|
end
|
||||||
|
Result := Void
|
||||||
|
end
|
||||||
|
end
|
||||||
|
sql_finalize
|
||||||
|
end
|
||||||
|
|
||||||
|
has_user_token (a_user: CMS_USER): BOOLEAN
|
||||||
|
-- Has the user `a_user' and associated session token?
|
||||||
|
local
|
||||||
|
l_parameters: STRING_TABLE [detachable ANY]
|
||||||
|
do
|
||||||
|
error_handler.reset
|
||||||
|
write_information_log (generator + ".has_user_token")
|
||||||
|
create l_parameters.make (1)
|
||||||
|
l_parameters.put (a_user.id, "uid")
|
||||||
|
sql_query (Select_user_token, l_parameters)
|
||||||
|
if not has_error and not sql_after then
|
||||||
|
if sql_read_integer_64 (1) = 1 then
|
||||||
|
Result := True
|
||||||
|
else
|
||||||
|
Result := False
|
||||||
|
end
|
||||||
|
end
|
||||||
|
sql_finalize
|
||||||
|
end
|
||||||
|
|
||||||
|
feature -- Change User token
|
||||||
|
|
||||||
|
new_user_session_auth (a_token: READABLE_STRING_GENERAL; a_user: CMS_USER;)
|
||||||
|
-- <Precursor>.
|
||||||
|
local
|
||||||
|
l_parameters: STRING_TABLE [detachable ANY]
|
||||||
|
do
|
||||||
|
error_handler.reset
|
||||||
|
write_information_log (generator + ".new_user_session")
|
||||||
|
create l_parameters.make (3)
|
||||||
|
l_parameters.put (a_user.id, "uid")
|
||||||
|
l_parameters.put (a_token, "token")
|
||||||
|
l_parameters.put (create {DATE_TIME}.make_now_utc, "utc_date")
|
||||||
|
sql_begin_transaction
|
||||||
|
sql_insert (sql_insert_session_auth, l_parameters)
|
||||||
|
sql_commit_transaction
|
||||||
|
sql_finalize
|
||||||
|
end
|
||||||
|
|
||||||
|
update_user_session_auth (a_token: READABLE_STRING_GENERAL; a_user: CMS_USER)
|
||||||
|
-- <Precursor>
|
||||||
|
local
|
||||||
|
l_parameters: STRING_TABLE [detachable ANY]
|
||||||
|
do
|
||||||
|
error_handler.reset
|
||||||
|
write_information_log (generator + ".update_user_session_auth")
|
||||||
|
create l_parameters.make (3)
|
||||||
|
l_parameters.put (a_user.id, "uid")
|
||||||
|
l_parameters.put (a_token, "token")
|
||||||
|
l_parameters.put (create {DATE_TIME}.make_now_utc, "utc_date")
|
||||||
|
sql_begin_transaction
|
||||||
|
sql_modify (sql_update_session_auth, l_parameters)
|
||||||
|
sql_commit_transaction
|
||||||
|
sql_finalize
|
||||||
|
end
|
||||||
|
|
||||||
|
feature {NONE} -- Implementation
|
||||||
|
|
||||||
|
fetch_user: detachable CMS_USER
|
||||||
|
local
|
||||||
|
l_id: INTEGER_64
|
||||||
|
l_name: detachable READABLE_STRING_32
|
||||||
|
do
|
||||||
|
if attached sql_read_integer_64 (1) as i then
|
||||||
|
l_id := i
|
||||||
|
end
|
||||||
|
if attached sql_read_string_32 (2) as s and then not s.is_whitespace then
|
||||||
|
l_name := s
|
||||||
|
end
|
||||||
|
if l_name /= Void then
|
||||||
|
create Result.make (l_name)
|
||||||
|
if l_id > 0 then
|
||||||
|
Result.set_id (l_id)
|
||||||
|
end
|
||||||
|
elseif l_id > 0 then
|
||||||
|
create Result.make_with_id (l_id)
|
||||||
|
end
|
||||||
|
if Result /= Void then
|
||||||
|
if attached sql_read_string (3) as l_password then
|
||||||
|
-- FIXME: should we return the password here ???
|
||||||
|
Result.set_hashed_password (l_password)
|
||||||
|
end
|
||||||
|
if attached sql_read_string (5) as l_email then
|
||||||
|
Result.set_email (l_email)
|
||||||
|
end
|
||||||
|
if attached sql_read_integer_32 (6) as l_status then
|
||||||
|
Result.set_status (l_status)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
check
|
||||||
|
expected_valid_user: False
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
feature {NONE} -- SQL statements
|
||||||
|
|
||||||
|
Select_user_by_token: STRING = "SELECT u.* FROM users as u JOIN session_auth as og ON og.uid = u.uid and og.access_token = :token;"
|
||||||
|
--| FIXME: replace the u.* by a list of field names, to avoid breaking `featch_user' if two fieds are swiped.
|
||||||
|
|
||||||
|
Sql_insert_session_auth: STRING = "INSERT INTO session_auth (uid, access_token, created) VALUES (:uid, :token, :utc_date);"
|
||||||
|
|
||||||
|
Sql_update_session_auth: STRING = "UPDATE session_auth SET access_token = :token, created = :utc_date WHERE uid =:uid;"
|
||||||
|
|
||||||
|
Select_user_token: STRING = "SELECT COUNT(*) FROM session_auth where uid = :uid;"
|
||||||
|
|
||||||
|
end
|
||||||
153
modules/session_auth/site/cms_token_generator.e
Normal file
153
modules/session_auth/site/cms_token_generator.e
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
note
|
||||||
|
description: "Provides security routine helpers"
|
||||||
|
date: "$Date$"
|
||||||
|
revision: "$Revision$"
|
||||||
|
|
||||||
|
class
|
||||||
|
CMS_TOKEN_GENERATOR
|
||||||
|
|
||||||
|
inherit
|
||||||
|
|
||||||
|
REFACTORING_HELPER
|
||||||
|
|
||||||
|
feature -- Access
|
||||||
|
|
||||||
|
token: STRING
|
||||||
|
-- Cryptographic random base 64 string.
|
||||||
|
do
|
||||||
|
Result := salt_with_size (16)
|
||||||
|
-- Remove trailing equal sign
|
||||||
|
Result.keep_head (Result.count - 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
salt: STRING
|
||||||
|
-- Cryptographic random number of 16 bytes.
|
||||||
|
do
|
||||||
|
Result := salt_with_size (16)
|
||||||
|
end
|
||||||
|
|
||||||
|
password: STRING
|
||||||
|
-- Cryptographic random password of 10 bytes.
|
||||||
|
do
|
||||||
|
Result := salt_with_size (10)
|
||||||
|
-- Remove trailing equal signs
|
||||||
|
Result.keep_head (Result.count - 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
password_hash (a_password, a_salt: STRING): STRING
|
||||||
|
-- Password hash based on password `a_password' and salt value `a_salt'.
|
||||||
|
do
|
||||||
|
Result := sha1_string (a_password + a_salt )
|
||||||
|
end
|
||||||
|
|
||||||
|
feature {NONE} -- Implementation
|
||||||
|
|
||||||
|
salt_with_size (a_val: INTEGER): STRING
|
||||||
|
-- Return a salt with size `a_val'.
|
||||||
|
local
|
||||||
|
l_salt: SALT_XOR_SHIFT_64_GENERATOR
|
||||||
|
l_array: ARRAY [INTEGER_8]
|
||||||
|
i: INTEGER
|
||||||
|
do
|
||||||
|
create l_salt.make (a_val)
|
||||||
|
create l_array.make_empty
|
||||||
|
i := 1
|
||||||
|
across
|
||||||
|
l_salt.new_sequence as c
|
||||||
|
loop
|
||||||
|
l_array.force (c.item.as_integer_8, i)
|
||||||
|
i := i + 1
|
||||||
|
end
|
||||||
|
Result := base_64 (l_array)
|
||||||
|
end
|
||||||
|
|
||||||
|
sha1_string (a_str: STRING): STRING
|
||||||
|
-- SHA1 diggest of `a_str'.
|
||||||
|
do
|
||||||
|
sha1.update_from_string (a_str)
|
||||||
|
Result := sha1.digest_as_string
|
||||||
|
sha1.reset
|
||||||
|
end
|
||||||
|
|
||||||
|
sha1: SHA1
|
||||||
|
-- Create a SHA1 object.
|
||||||
|
do
|
||||||
|
create Result.make
|
||||||
|
end
|
||||||
|
|
||||||
|
feature -- Encoding
|
||||||
|
|
||||||
|
|
||||||
|
base_64 (bytes: SPECIAL [INTEGER_8]): STRING_8
|
||||||
|
-- Encodes a byte array into a STRING doing base64 encoding.
|
||||||
|
local
|
||||||
|
l_output: SPECIAL [INTEGER_8]
|
||||||
|
l_remaining: INTEGER
|
||||||
|
i, ptr: INTEGER
|
||||||
|
char: CHARACTER
|
||||||
|
do
|
||||||
|
to_implement ("Check existing code to do that!!!.")
|
||||||
|
create l_output.make_filled (0, ((bytes.count + 2) // 3) * 4)
|
||||||
|
l_remaining := bytes.count
|
||||||
|
from
|
||||||
|
i := 0
|
||||||
|
ptr := 0
|
||||||
|
until
|
||||||
|
l_remaining <= 3
|
||||||
|
loop
|
||||||
|
l_output [ptr] := encode_value (bytes [i] |>> 2)
|
||||||
|
ptr := ptr + 1
|
||||||
|
l_output [ptr] := encode_value (((bytes [i] & 0x3) |<< 4) | ((bytes [i + 1] |>> 4) & 0xF))
|
||||||
|
ptr := ptr + 1
|
||||||
|
l_output [ptr] := encode_value (((bytes [i + 1] & 0xF) |<< 2) | ((bytes [i + 2] |>> 6) & 0x3))
|
||||||
|
ptr := ptr + 1
|
||||||
|
l_output [ptr] := encode_value (bytes [i + 2] & 0x3F)
|
||||||
|
ptr := ptr + 1
|
||||||
|
l_remaining := l_remaining - 3
|
||||||
|
i := i + 3
|
||||||
|
end
|
||||||
|
-- encode when exactly 1 element (left) to encode
|
||||||
|
char := '='
|
||||||
|
if l_remaining = 1 then
|
||||||
|
l_output [ptr] := encode_value (bytes [i] |>> 2)
|
||||||
|
ptr := ptr + 1
|
||||||
|
l_output [ptr] := encode_value (((bytes [i]) & 0x3) |<< 4)
|
||||||
|
ptr := ptr + 1
|
||||||
|
l_output [ptr] := char.code.as_integer_8
|
||||||
|
ptr := ptr + 1
|
||||||
|
l_output [ptr] := char.code.as_integer_8
|
||||||
|
ptr := ptr + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- encode when exactly 2 elements (left) to encode
|
||||||
|
if l_remaining = 2 then
|
||||||
|
l_output [ptr] := encode_value (bytes [i] |>> 2)
|
||||||
|
ptr := ptr + 1
|
||||||
|
l_output [ptr] := encode_value (((bytes [i] & 0x3) |<< 4) | ((bytes [i + 1] |>> 4) & 0xF));
|
||||||
|
ptr := ptr + 1
|
||||||
|
l_output [ptr] := encode_value ((bytes [i + 1] & 0xF) |<< 2);
|
||||||
|
ptr := ptr + 1
|
||||||
|
l_output [ptr] := char.code.as_integer_8
|
||||||
|
ptr := ptr + 1
|
||||||
|
end
|
||||||
|
create Result.make_empty
|
||||||
|
across
|
||||||
|
l_output as elem
|
||||||
|
loop
|
||||||
|
Result.append_character (elem.item.to_character_8)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
base64_map: SPECIAL [CHARACTER_8]
|
||||||
|
-- Table for Base64 encoding.
|
||||||
|
once
|
||||||
|
Result := ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/").area
|
||||||
|
end
|
||||||
|
|
||||||
|
encode_value (i: INTEGER_8): INTEGER_8
|
||||||
|
-- Encode `i'.
|
||||||
|
do
|
||||||
|
Result := base64_map [i & 0x3F].code.as_integer_8
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
11
modules/session_auth/site/scripts/session_auth_table.sql.tpl
Normal file
11
modules/session_auth/site/scripts/session_auth_table.sql.tpl
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
CREATE TABLE session_auth (
|
||||||
|
`uid` INTEGER PRIMARY KEY NOT NULL CHECK(`uid`>=0),
|
||||||
|
`access_token` TEXT NOT NULL,
|
||||||
|
`created` DATETIME NOT NULL,
|
||||||
|
CONSTRAINT `uid`
|
||||||
|
UNIQUE(`uid`),
|
||||||
|
CONSTRAINT `access_token`
|
||||||
|
UNIQUE(`access_token`)
|
||||||
|
);
|
||||||
|
|
||||||
37
modules/session_auth/site/templates/block_login.tpl
Normal file
37
modules/session_auth/site/templates/block_login.tpl
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<div class="primary-tabs">
|
||||||
|
{unless isset="$user"}
|
||||||
|
<h3>Login or <a href="{$site_url/}account/roc-register">Register</a></h3>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<form name="cms_session_auth" action="{$site_url/}account/login-with-session" method="POST">
|
||||||
|
<div>
|
||||||
|
<input type="text" name="username" id="username" required value="{$username/}">
|
||||||
|
<label>Username</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input type="password" name="password" id="password" required >
|
||||||
|
<label>Password</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<a href="{$site_url/}account/new-password">Forgot password?</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/unless}
|
||||||
|
{if isset=$error}
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<strong>{$error/}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -109,6 +109,17 @@ feature -- Access node
|
|||||||
Result := taxonomy_storage.term_count_from_vocabulary (a_vocab)
|
Result := taxonomy_storage.term_count_from_vocabulary (a_vocab)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
terms_of_content (a_content: CMS_CONTENT; a_vocabulary: detachable CMS_VOCABULARY): detachable CMS_TERM_COLLECTION
|
||||||
|
-- Terms related to `a_content', and if `a_vocabulary' is set
|
||||||
|
-- constrain to be part of `a_vocabulary'.
|
||||||
|
require
|
||||||
|
content_with_identifier: a_content.has_identifier
|
||||||
|
do
|
||||||
|
if attached a_content.identifier as l_id then
|
||||||
|
Result := terms_of_entity (a_content.content_type, l_id, a_vocabulary)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
terms_of_entity (a_type_name: READABLE_STRING_GENERAL; a_entity: READABLE_STRING_GENERAL; a_vocabulary: detachable CMS_VOCABULARY): detachable CMS_TERM_COLLECTION
|
terms_of_entity (a_type_name: READABLE_STRING_GENERAL; a_entity: READABLE_STRING_GENERAL; a_vocabulary: detachable CMS_VOCABULARY): detachable CMS_TERM_COLLECTION
|
||||||
-- Terms related to `(a_type_name,a_entity)', and if `a_vocabulary' is set
|
-- Terms related to `(a_type_name,a_entity)', and if `a_vocabulary' is set
|
||||||
-- constrain to be part of `a_vocabulary'.
|
-- constrain to be part of `a_vocabulary'.
|
||||||
@@ -179,6 +190,30 @@ feature -- Write
|
|||||||
error_handler.append (taxonomy_storage.error_handler)
|
error_handler.append (taxonomy_storage.error_handler)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
associate_term_with_content (a_term: CMS_TERM; a_content: CMS_CONTENT)
|
||||||
|
-- Associate term `a_term' with `a_content'.
|
||||||
|
require
|
||||||
|
content_with_identifier: a_content.has_identifier
|
||||||
|
do
|
||||||
|
reset_error
|
||||||
|
if attached a_content.identifier as l_id then
|
||||||
|
taxonomy_storage.associate_term_with_entity (a_term, a_content.content_type, l_id)
|
||||||
|
error_handler.append (taxonomy_storage.error_handler)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
unassociate_term_from_content (a_term: CMS_TERM; a_content: CMS_CONTENT)
|
||||||
|
-- Unassociate term `a_term' from `a_content'.
|
||||||
|
require
|
||||||
|
content_with_identifier: a_content.has_identifier
|
||||||
|
do
|
||||||
|
reset_error
|
||||||
|
if attached a_content.identifier as l_id then
|
||||||
|
taxonomy_storage.unassociate_term_from_entity (a_term, a_content.content_type, l_id)
|
||||||
|
error_handler.append (taxonomy_storage.error_handler)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
associate_term_with_entity (a_term: CMS_TERM; a_type_name: READABLE_STRING_GENERAL; a_entity: READABLE_STRING_GENERAL)
|
associate_term_with_entity (a_term: CMS_TERM; a_type_name: READABLE_STRING_GENERAL; a_entity: READABLE_STRING_GENERAL)
|
||||||
-- Associate term `a_term' with `(a_type_name, a_entity)'.
|
-- Associate term `a_term' with `(a_type_name, a_entity)'.
|
||||||
do
|
do
|
||||||
@@ -215,6 +250,275 @@ feature -- Write
|
|||||||
error_handler.append (taxonomy_storage.error_handler)
|
error_handler.append (taxonomy_storage.error_handler)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
feature -- Web forms
|
||||||
|
|
||||||
|
populate_edit_form (a_response: CMS_RESPONSE; a_form: CMS_FORM; a_content_type_name: READABLE_STRING_8; a_content: detachable CMS_CONTENT)
|
||||||
|
local
|
||||||
|
ti: detachable WSF_FORM_TEXT_INPUT
|
||||||
|
th: WSF_FORM_HIDDEN_INPUT
|
||||||
|
w_set: WSF_FORM_FIELD_SET
|
||||||
|
w_select: WSF_FORM_SELECT
|
||||||
|
w_opt: WSF_FORM_SELECT_OPTION
|
||||||
|
w_cb: WSF_FORM_CHECKBOX_INPUT
|
||||||
|
w_voc_set: WSF_FORM_FIELD_SET
|
||||||
|
s: STRING_32
|
||||||
|
voc: CMS_VOCABULARY
|
||||||
|
t: detachable CMS_TERM
|
||||||
|
l_terms: detachable CMS_TERM_COLLECTION
|
||||||
|
l_has_edit_permission: BOOLEAN
|
||||||
|
do
|
||||||
|
if
|
||||||
|
attached vocabularies_for_type (a_content_type_name) as l_vocs and then not l_vocs.is_empty
|
||||||
|
then
|
||||||
|
l_has_edit_permission := a_response.has_permissions (<<"update any taxonomy", "update " + a_content_type_name + " taxonomy">>)
|
||||||
|
|
||||||
|
-- Handle Taxonomy fields, if any associated with `content_type'.
|
||||||
|
create w_set.make
|
||||||
|
w_set.add_css_class ("taxonomy")
|
||||||
|
l_vocs.sort
|
||||||
|
across
|
||||||
|
l_vocs as vocs_ic
|
||||||
|
loop
|
||||||
|
voc := vocs_ic.item
|
||||||
|
create th.make_with_text ({STRING_32} "taxonomy_vocabularies[" + voc.id.out + "]", voc.name)
|
||||||
|
w_set.extend (th)
|
||||||
|
|
||||||
|
l_terms := Void
|
||||||
|
if a_content /= Void then
|
||||||
|
l_terms := terms_of_content (a_content, voc)
|
||||||
|
if l_terms /= Void then
|
||||||
|
l_terms.sort
|
||||||
|
end
|
||||||
|
end
|
||||||
|
create w_voc_set.make
|
||||||
|
w_set.extend (w_voc_set)
|
||||||
|
|
||||||
|
if voc.is_tags then
|
||||||
|
w_voc_set.set_legend (cms_api.translation (voc.name, Void))
|
||||||
|
|
||||||
|
create ti.make ({STRING_32} "taxonomy_" + voc.id.out)
|
||||||
|
w_voc_set.extend (ti)
|
||||||
|
if voc.is_term_required then
|
||||||
|
ti.enable_required
|
||||||
|
end
|
||||||
|
if attached voc.description as l_desc then
|
||||||
|
ti.set_description (cms_api.html_encoded (cms_api.translation (l_desc, Void)))
|
||||||
|
else
|
||||||
|
ti.set_description (a_response.html_encoded (cms_api.translation (voc.name, Void)))
|
||||||
|
end
|
||||||
|
ti.set_size (70)
|
||||||
|
if l_terms /= Void then
|
||||||
|
create s.make_empty
|
||||||
|
across
|
||||||
|
l_terms as ic
|
||||||
|
loop
|
||||||
|
t := ic.item
|
||||||
|
if not s.is_empty then
|
||||||
|
s.append_character (',')
|
||||||
|
s.append_character (' ')
|
||||||
|
end
|
||||||
|
if ic.item.text.has (' ') then
|
||||||
|
s.append_character ('"')
|
||||||
|
s.append (t.text)
|
||||||
|
s.append_character ('"')
|
||||||
|
else
|
||||||
|
s.append (t.text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ti.set_text_value (s)
|
||||||
|
end
|
||||||
|
if not l_has_edit_permission then
|
||||||
|
ti.set_is_readonly (True)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
fill_vocabularies_with_terms (voc)
|
||||||
|
if not voc.terms.is_empty then
|
||||||
|
if voc.multiple_terms_allowed then
|
||||||
|
if attached voc.description as l_desc then
|
||||||
|
w_voc_set.set_legend (cms_api.html_encoded (l_desc))
|
||||||
|
else
|
||||||
|
w_voc_set.set_legend (cms_api.html_encoded (voc.name))
|
||||||
|
end
|
||||||
|
across
|
||||||
|
voc as voc_terms_ic
|
||||||
|
loop
|
||||||
|
t := voc_terms_ic.item
|
||||||
|
create w_cb.make_with_value ({STRING_32} "taxonomy_" + voc.id.out + "[]", t.text)
|
||||||
|
w_cb.set_title (t.text)
|
||||||
|
w_voc_set.extend (w_cb)
|
||||||
|
if l_terms /= Void and then across l_terms as ic some ic.item.text.same_string (t.text) end then
|
||||||
|
w_cb.set_checked (True)
|
||||||
|
end
|
||||||
|
if not l_has_edit_permission then
|
||||||
|
w_cb.set_is_readonly (True)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
create w_select.make ({STRING_32} "taxonomy_" + voc.id.out)
|
||||||
|
w_voc_set.extend (w_select)
|
||||||
|
|
||||||
|
if attached voc.description as l_desc then
|
||||||
|
w_select.set_description (cms_api.html_encoded (l_desc))
|
||||||
|
else
|
||||||
|
w_select.set_description (cms_api.html_encoded (voc.name))
|
||||||
|
end
|
||||||
|
w_voc_set.set_legend (cms_api.html_encoded (voc.name))
|
||||||
|
|
||||||
|
across
|
||||||
|
voc as voc_terms_ic
|
||||||
|
loop
|
||||||
|
t := voc_terms_ic.item
|
||||||
|
create w_opt.make (cms_api.html_encoded (t.text), cms_api.html_encoded (t.text))
|
||||||
|
w_select.add_option (w_opt)
|
||||||
|
|
||||||
|
if l_terms /= Void and then across l_terms as ic some ic.item.text.same_string (t.text) end then
|
||||||
|
w_opt.set_is_selected (True)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not l_has_edit_permission then
|
||||||
|
w_select.set_is_readonly (True)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
a_form.submit_actions.extend (agent taxonomy_submit_action (a_response, Current, l_vocs, a_content, ?))
|
||||||
|
|
||||||
|
if
|
||||||
|
attached a_form.fields_by_name ("title") as l_title_fields and then
|
||||||
|
attached l_title_fields.first as l_title_field
|
||||||
|
then
|
||||||
|
a_form.insert_after (w_set, l_title_field)
|
||||||
|
else
|
||||||
|
a_form.extend (w_set)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
taxonomy_submit_action (a_response: CMS_RESPONSE; a_taxonomy_api: CMS_TAXONOMY_API; a_vocs: CMS_VOCABULARY_COLLECTION; a_content: detachable CMS_CONTENT fd: WSF_FORM_DATA)
|
||||||
|
require
|
||||||
|
vocs_not_empty: not a_vocs.is_empty
|
||||||
|
local
|
||||||
|
l_voc_name: READABLE_STRING_32
|
||||||
|
l_terms_to_remove: ARRAYED_LIST [CMS_TERM]
|
||||||
|
l_new_terms: LIST [READABLE_STRING_32]
|
||||||
|
l_text: READABLE_STRING_GENERAL
|
||||||
|
l_found: BOOLEAN
|
||||||
|
t: detachable CMS_TERM
|
||||||
|
vid: INTEGER_64
|
||||||
|
do
|
||||||
|
if
|
||||||
|
a_content /= Void and then a_content.has_identifier and then
|
||||||
|
attached fd.table_item ("taxonomy_vocabularies") as fd_vocs
|
||||||
|
then
|
||||||
|
if a_response.has_permissions (<<{STRING_32} "update any taxonomy", {STRING_32} "update " + a_content.content_type + " taxonomy">>) then
|
||||||
|
across
|
||||||
|
fd_vocs.values as ic
|
||||||
|
loop
|
||||||
|
vid := ic.key.to_integer_64
|
||||||
|
l_voc_name := ic.item.string_representation
|
||||||
|
|
||||||
|
if attached a_vocs.item_by_id (vid) as voc then
|
||||||
|
if attached fd.string_item ("taxonomy_" + vid.out) as l_string then
|
||||||
|
l_new_terms := a_taxonomy_api.splitted_string (l_string, ',')
|
||||||
|
elseif attached fd.table_item ("taxonomy_" + vid.out) as fd_terms then
|
||||||
|
create {ARRAYED_LIST [READABLE_STRING_32]} l_new_terms.make (fd_terms.count)
|
||||||
|
across
|
||||||
|
fd_terms as t_ic
|
||||||
|
loop
|
||||||
|
l_new_terms.force (t_ic.item.string_representation)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
create {ARRAYED_LIST [READABLE_STRING_32]} l_new_terms.make (0)
|
||||||
|
end
|
||||||
|
|
||||||
|
create l_terms_to_remove.make (0)
|
||||||
|
if attached a_taxonomy_api.terms_of_content (a_content, voc) as l_existing_terms then
|
||||||
|
across
|
||||||
|
l_existing_terms as t_ic
|
||||||
|
loop
|
||||||
|
l_text := t_ic.item.text
|
||||||
|
from
|
||||||
|
l_found := False
|
||||||
|
l_new_terms.start
|
||||||
|
until
|
||||||
|
l_new_terms.after
|
||||||
|
loop
|
||||||
|
if l_new_terms.item.same_string_general (l_text) then
|
||||||
|
-- Already associated with term `t_ic.text'.
|
||||||
|
l_found := True
|
||||||
|
l_new_terms.remove
|
||||||
|
else
|
||||||
|
l_new_terms.forth
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not l_found then
|
||||||
|
-- Remove term
|
||||||
|
l_terms_to_remove.force (t_ic.item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
across
|
||||||
|
l_terms_to_remove as t_ic
|
||||||
|
loop
|
||||||
|
a_taxonomy_api.unassociate_term_from_content (t_ic.item, a_content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
across
|
||||||
|
l_new_terms as t_ic
|
||||||
|
loop
|
||||||
|
t := a_taxonomy_api.term_by_text (t_ic.item, voc)
|
||||||
|
if
|
||||||
|
t = Void and voc.is_tags
|
||||||
|
then
|
||||||
|
-- Create new term!
|
||||||
|
create t.make (t_ic.item)
|
||||||
|
a_taxonomy_api.save_term (t, voc)
|
||||||
|
if a_taxonomy_api.has_error then
|
||||||
|
t := Void
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if t /= Void then
|
||||||
|
a_taxonomy_api.associate_term_with_content (t, a_content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
append_taxonomy_to_xhtml (a_content: CMS_CONTENT; a_response: CMS_RESPONSE; a_output: STRING)
|
||||||
|
-- Append taxonomy related to `a_content' to xhtml string `a_output',
|
||||||
|
-- using `a_response' helper routines.
|
||||||
|
do
|
||||||
|
if
|
||||||
|
attached vocabularies_for_type (a_content.content_type) as vocs and then not vocs.is_empty
|
||||||
|
then
|
||||||
|
vocs.sort
|
||||||
|
across
|
||||||
|
vocs as ic
|
||||||
|
loop
|
||||||
|
if
|
||||||
|
attached terms_of_content (a_content, ic.item) as l_terms and then
|
||||||
|
not l_terms.is_empty
|
||||||
|
then
|
||||||
|
a_output.append ("<ul class=%"taxonomy term-" + ic.item.id.out + "%">")
|
||||||
|
a_output.append (cms_api.html_encoded (ic.item.name))
|
||||||
|
a_output.append (": ")
|
||||||
|
across
|
||||||
|
l_terms as t_ic
|
||||||
|
loop
|
||||||
|
a_output.append ("<li>")
|
||||||
|
a_response.append_link_to_html (t_ic.item.text, "taxonomy/term/" + t_ic.item.id.out, Void, a_output)
|
||||||
|
a_output.append ("</li>")
|
||||||
|
end
|
||||||
|
a_output.append ("</ul>%N")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
feature -- Helpers
|
feature -- Helpers
|
||||||
|
|
||||||
splitted_string (s: READABLE_STRING_32; sep: CHARACTER): LIST [READABLE_STRING_32]
|
splitted_string (s: READABLE_STRING_32; sep: CHARACTER): LIST [READABLE_STRING_32]
|
||||||
|
|||||||
@@ -292,6 +292,26 @@ feature -- Logging
|
|||||||
end
|
end
|
||||||
end
|
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
|
||||||
|
to_implement ("Implement i18n support [2015-may]")
|
||||||
|
Result := a_text.as_string_32
|
||||||
|
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"
|
||||||
|
local
|
||||||
|
l_formatter: CMS_STRING_FORMATTER
|
||||||
|
do
|
||||||
|
create l_formatter
|
||||||
|
Result := l_formatter.formatted_string (a_text, args)
|
||||||
|
end
|
||||||
|
|
||||||
feature -- Emails
|
feature -- Emails
|
||||||
|
|
||||||
new_email (a_to_address: READABLE_STRING_8; a_subject: READABLE_STRING_8; a_content: READABLE_STRING_8): CMS_EMAIL
|
new_email (a_to_address: READABLE_STRING_8; a_subject: READABLE_STRING_8; a_content: READABLE_STRING_8): CMS_EMAIL
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ inherit
|
|||||||
|
|
||||||
feature -- Access
|
feature -- Access
|
||||||
|
|
||||||
|
identifier: detachable READABLE_STRING_32
|
||||||
|
-- Optional identifier.
|
||||||
|
deferred
|
||||||
|
end
|
||||||
|
|
||||||
title: detachable READABLE_STRING_32
|
title: detachable READABLE_STRING_32
|
||||||
-- Title associated with Current content.
|
-- Title associated with Current content.
|
||||||
deferred
|
deferred
|
||||||
@@ -37,6 +42,14 @@ feature -- Access
|
|||||||
|
|
||||||
feature -- Status report
|
feature -- Status report
|
||||||
|
|
||||||
|
has_identifier: BOOLEAN
|
||||||
|
-- Current content has identifier?
|
||||||
|
do
|
||||||
|
Result := identifier /= Void
|
||||||
|
ensure
|
||||||
|
Result implies identifier /= Void
|
||||||
|
end
|
||||||
|
|
||||||
is_typed_as (a_content_type: READABLE_STRING_GENERAL): BOOLEAN
|
is_typed_as (a_content_type: READABLE_STRING_GENERAL): BOOLEAN
|
||||||
-- Is current node of type `a_content_type' ?
|
-- Is current node of type `a_content_type' ?
|
||||||
do
|
do
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ feature {NONE} -- Initialization
|
|||||||
|
|
||||||
feature -- Access
|
feature -- Access
|
||||||
|
|
||||||
|
identifier: detachable READABLE_STRING_32
|
||||||
|
-- <Precursor>
|
||||||
|
|
||||||
title: detachable READABLE_STRING_32
|
title: detachable READABLE_STRING_32
|
||||||
-- Title associated with Current content.
|
-- Title associated with Current content.
|
||||||
|
|
||||||
@@ -42,6 +45,15 @@ feature -- Access
|
|||||||
|
|
||||||
feature -- Element change
|
feature -- Element change
|
||||||
|
|
||||||
|
set_identifier (a_identifier: detachable READABLE_STRING_GENERAL)
|
||||||
|
do
|
||||||
|
if a_identifier = Void then
|
||||||
|
identifier := Void
|
||||||
|
else
|
||||||
|
create {IMMUTABLE_STRING_32} identifier.make_from_string_general (a_identifier)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
set_title (a_title: detachable READABLE_STRING_GENERAL)
|
set_title (a_title: detachable READABLE_STRING_GENERAL)
|
||||||
do
|
do
|
||||||
if a_title = Void then
|
if a_title = Void then
|
||||||
|
|||||||
@@ -113,26 +113,6 @@ feature -- Access
|
|||||||
end
|
end
|
||||||
end
|
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
|
|
||||||
to_implement ("Implement i18n support [2015-may]")
|
|
||||||
Result := a_text.as_string_32
|
|
||||||
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"
|
|
||||||
local
|
|
||||||
l_formatter: CMS_STRING_FORMATTER
|
|
||||||
do
|
|
||||||
create l_formatter
|
|
||||||
Result := l_formatter.formatted_string (a_text, args)
|
|
||||||
end
|
|
||||||
|
|
||||||
feature -- API
|
feature -- API
|
||||||
|
|
||||||
api: CMS_API
|
api: CMS_API
|
||||||
@@ -896,6 +876,22 @@ feature -- Menu: change
|
|||||||
m.extend (lnk)
|
m.extend (lnk)
|
||||||
end
|
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
|
feature -- Message
|
||||||
|
|
||||||
add_message (a_msg: READABLE_STRING_8; a_category: detachable READABLE_STRING_8)
|
add_message (a_msg: READABLE_STRING_8; a_category: detachable READABLE_STRING_8)
|
||||||
|
|||||||
Reference in New Issue
Block a user