module Zena
module Use
module Urls
NODE_ACTIONS = {
'' => {:url => '/nodes/#{node_zip}'},
'drive' => {:url => '/nodes/#{node_zip}/edit'},
'add_doc' => {:url => '/documents/new', :query => {:parent_id => 'node_zip'}},
'destroy' => {:url => '/nodes/#{node_zip}', :method => 'delete'},
'update' => {:url => '/nodes/#{node_zip}', :method => 'put'},
'drop' => {:url => '/nodes/#{node_zip}/drop'},
'unlink' => {:url => '/nodes/#{node_zip}/link/#{node.link_id}', :method => 'delete'},
'zafu' => {:url => '/nodes/#{node_zip}/zafu'},
'publish' => {:url => '/nodes/#{node_zip}/versions/0/publish', :method => 'put'},
'propose' => {:url => '/nodes/#{node_zip}/versions/0/propose', :method => 'put'},
'refuse' => {:url => '/nodes/#{node_zip}/versions/0/refuse', :method => 'put'},
'edit' => {:url => '/nodes/#{node_zip}/versions/0/edit'},
'create' => {:url => '/nodes', :method => 'post', :query => {:parent_id => 'node_zip'}},
}
ALLOWED_REGEXP = /\A(([a-zA-Z]+)([0-9]+)|([#{String::ALLOWED_CHARS_IN_URL}\-%]+))(_[a-zA-Z]+|)(=[a-z0-9]+|)(\..+|)\Z/
module Common
# This is directly related to the FileMatch clause in httpd.rhtml (mod_expires for apaches)
CACHESTAMP_FORMATS = %w{ico flv jpg jpeg png gif js css swf}
def prefix
if visitor.is_anon?
visitor.lang
else
AUTHENTICATED_PREFIX
end
end
# We overwrite some url writers that might use Node so that they use
# zip instead of id.
NODE_ACTIONS.each do |name, definition|
method = name.blank? ? 'node_path' : "#{name}_node_path"
hash_str = []
if query = definition[:query]
query.each do |k,v|
hash_str << ":#{k} => #{v}"
end
end
if hash_str.empty?
opts_merge = ''
else
opts_merge = "options = {#{hash_str.join(',')}}.merge(options)" # {:parent_id => node_zip}.merge(options)
end
class_eval(%Q{
def #{method}(node, options={}) # def zafu_node_path(node, options={})
return '#' unless node # return '#' unless node
node_zip = node.kind_of?(Node) ? node.zip : node # node_zip = node.kind_of?(Node) ? node.zip : node
#{opts_merge} # options = {:parent_id => node.zip}.merge(options)
append_query_params("#{definition[:url]}", options) # append_query_params("/nodes/\#{node.zip}/zafu", options)
end # end
}, __FILE__, __LINE__ - 5)
end
# Path to remove a node link.
def unlink_node_path(node, options={})
return '#' unless node.can_write? && node.link_id
node_link_path(node.zip, node.link_id, options)
end
# Path for a node. Options can be :format, :host and :mode.
# ex '/en/document34_print.html'
def zen_path(node, options={})
return '#' unless node
if anchor = options.delete(:anchor)
return "#{zen_path(node, options)}##{anchor}"
end
opts = options.dup
format = opts.delete(:format)
if format.blank?
format = 'html'
elsif format == 'data'
if node.kind_of?(Document)
format = node.ext
else
format = 'html'
end
end
pre = opts.delete(:prefix) || (visitor.is_anon? && opts.delete(:lang)) || prefix
mode = opts.delete(:mode)
if ep = opts[:encode_params]
ep = ep.split(',').map(&:strip)
if ep.delete('mode')
mode ||= params[:mode]
end
opts['encode_params'] = ep
end
if host = opts.delete(:host)
if ssl = opts.delete(:ssl)
http = 'https'
else
http = http_protocol
end
abs_url_prefix = "#{http}://#{host}"
else
abs_url_prefix = ''
end
if node.kind_of?(Document) && format == node.ext
if node.v_public? && !visitor.site.authentication?
# force the use of a cacheable path for the data, even when navigating in '/oo'
pre = node.version.lang
end
end
if asset = opts.delete(:asset)
mode = nil
end
if should_cachestamp?(node, format, asset)
stamp = make_cachestamp(node, mode)
end
path = if !asset && node[:id] == visitor.site[:root_id] && mode.nil? && format == 'html'
"#{abs_url_prefix}/#{pre}" # index page
elsif node[:custom_base]
"#{abs_url_prefix}/#{pre}/" +
basepath_as_url(node.basepath) +
(mode ? "_#{mode}" : '') +
(asset ? "=#{asset}" : '') +
(stamp ? ".#{stamp}" : '') +
(format == 'html' ? '' : ".#{format}")
else
"#{abs_url_prefix}/#{pre}/" +
(node.basepath.blank? ? '' : "#{basepath_as_url(node.basepath)}/") +
(node.klass.downcase ) +
(node[:zip].to_s ) +
(mode ? "_#{mode}" : '') +
(asset ? "=#{asset}" : '') +
(stamp ? ".#{stamp}" : '') +
".#{format}"
end
append_query_params(path, opts)
end
def basepath_as_url(path)
path.split('/').map do |zip|
if n = secure(Node) { Node.find_by_zip(zip) }
n.title.url_name
else
nil
end
end.compact.join('/')
end
def append_query_params(path, opts)
if opts == {}
path
else
tz = opts.delete(:tz)
list = opts.keys.map do |k|
# FIXME: DOC
if k.to_s == 'encode_params'
opts[k].map do |key|
value = params[key]
if value.kind_of?(Hash)
{key => value}.to_query
elsif value.kind_of?(Array)
{key => value.map{|v| v.blank? ? nil : v}.compact}.to_query
elsif !value.blank?
"#{key}=#{CGI.escape(value)}"
else
nil
end
end
elsif value = opts[k]
if value.respond_to?(:strftime_tz)
"#{k}=#{CGI.escape(value.strftime_tz(_(Zena::Use::Dates::DATETIME), tz))}"
elsif value.kind_of?(Hash)
"#{k}=#{value.to_query}"
elsif value.kind_of?(Node)
"#{k}=#{value.zip}"
elsif !value.nil?
"#{k}=#{CGI.escape(value.to_s)}"
else
nil
end
else
nil
end
end.flatten.compact
# TODO: replace '&' by '&' ? Or escape later ? Use h before zen_path in templates ? What about css/xls/other stuff ?
# Best solution: use 'h' in template when set in default
path + (list.empty? ? '' : "?#{list.sort.join('&')}")
end
end
# Url for a node. Options are 'mode' and 'format'
# ex 'http://test.host/en/document34_print.html'
def zen_url(node, opts={})
zen_path(node,{:host => host_with_port}.merge(opts))
end
# Return the path to a document's data
def data_path(node, opts={})
if node.kind_of?(Document)
zen_path(node, opts.merge(:format => node.prop['ext']))
else
zen_path(node, opts)
end
end
def cachestamp_format?(format)
CACHESTAMP_FORMATS.include?(format)
end
def should_cachestamp?(node, format, asset)
cachestamp_format?(format)
# &&
# ((node.kind_of?(Document) && node.prop['ext'] == format) || asset)
end
def make_cachestamp(node, mode)
str = if mode
if node.kind_of?(Image)
if iformat = Iformat[mode]
"#{node.updated_at.to_i + iformat[:hash_id]}"
else
# random (will raise a 404 error anyway)
"#{node.updated_at.to_i + Time.now.to_i}"
end
else
# same format but different mode ? foobar_iphone.css ?
# will not be used.
node.updated_at.to_i.to_s
end
else
node.updated_at.to_i.to_s
end
Digest::SHA1.hexdigest(str)[0..4]
end
# Url parameters (without format/mode/prefix...)
def query_params
res = {}
path_params.each do |k,v|
next if [:mode, :format, :asset, :cachestamp].include?(k.to_sym)
res[k.to_sym] = v
end
res
end
# Url parameters (without action,controller,path,prefix)
def path_params
res = {}
params.each do |k,v|
next if [:action, :controller, :path, :prefix, :id].include?(k.to_sym)
res[k.to_sym] = v
end
res
end
def http_protocol
'http'
end
# We do not have access to the request. Port and host should be passed from view.
def host_with_port
current_site.host
end
end # Common
module ViewAndControllerMethods
def host_with_port
@host_with_port ||= begin
port = request.port
if port.blank? || port.to_s == '80'
current_site.host
else
"#{current_site.host}:#{port}"
end
end
end
def http_protocol
@http_protocol ||= begin
if request.protocol =~ /^(.*):\/\/$/
$1
else
'http'
end
end
end
end
module ControllerMethods
include Common
include ViewAndControllerMethods
end # ControllerMethods
module ViewMethods
include Common
include ViewAndControllerMethods
include RubyLess
safe_method [:url, Node] => {:class => String, :method => 'zen_url'}
safe_method [:url, Node, Hash] => {:class => String, :method => 'zen_url'}
safe_method [:path, Node] => {:class => String, :method => 'zen_path'}
safe_method [:path, Node, Hash] => {:class => String, :method => 'zen_path'}
safe_method [:zen_path, Node, Hash] => {:class => String, :accept_nil => true}
safe_method [:zen_path, Node] => {:class => String, :accept_nil => true}
safe_method [:zen_path, String, Hash] => {:class => String, :accept_nil => true, :method => 'dummy_zen_path'}
safe_method [:zen_path, String] => {:class => String, :accept_nil => true, :method => 'dummy_zen_path'}
NODE_ACTIONS.keys.each do |action|
next if action.blank?
safe_method [:"#{action}_node_path", Node, Hash] => {:class => String, :accept_nil => true}
safe_method [:"#{action}_node_path", Node] => {:class => String, :accept_nil => true}
end
safe_method :start_id => {:class => Number, :method => 'start_node_zip'}
def dummy_zen_path(string, options = {})
if anchor = options.delete(:anchor)
"#{string}##{anchor}"
else
"#{string}"
end
end
end # ViewMethods
module ZafuMethods
include RubyLess
# private
safe_method :insert_dom_id => :insert_dom_id
# Add the dom_id inside a RubyLess built method (used with make_href and ajax).
#
def insert_dom_id(signature)
return nil if signature.size != 1
{:method => @insert_dom_id, :class => String}
end
# creates a link. Options are:
# :href (node, parent, project, root)
# :tattr (translated attribute used as text link)
# :attr (attribute used as text link)
#
#
#
#
def r_link
# If we have a contextual timezone set, pass it to @params
if tz_name = @params[:tz]
tz_result, tz_var = set_tz_var(tz_name)
return tz_result unless tz_var
@params[:tz] = 'tz'
elsif tz_var = get_context_var('set_var', 'tz')
@params[:tz] = 'tz'
end
if @params[:page] && @params[:page] != '[page_page]' # lets users use 'page' as pagination key
pagination_links
else
make_link
end
end
# Insert a named anchor
def r_anchor
@params[:anchor] ||= 'true'
r_link
end
# Create a link tag.
#
# ==== Parameters (hash)
#
# * +:update+ - DOM_ID: produce an Ajax call that will update this part of the page (optional)
# * +:default_text+ - default text to use for the link if there are no 'text', 'eval' or 'attr' params
# * +:action+ - link action (edit, show, etc)
#
def make_link(options = {})
remote_target = (options[:update] || @params.delete(:update))
options[:action] ||= @params.delete(:action)
confirm = @params.delete(:confirm)
@markup.tag ||= 'a'
if @markup.tag == 'a'
markup = @markup
else
markup = Zafu::Markup.new('a')
end
steal_and_eval_html_params_for(markup, @params)
href = make_href(remote_target, options)
# This is to make sure live_id is set *inside* the tag.
if @live_param
text = add_live_id(text_for_link, markup)
@live_param = nil
else
text = text_for_link(options[:default_text])
end
http_method = http_method_from_action(options[:action])
if http_method == 'delete' && method != 'unlink'
confirm ||= '#{t("Destroy")} "#{h title}" ?'
end
if confirm
confirm = ::RubyLess.translate_string(self, confirm)
if confirm.literal
markup.set_param(:"data-confirm", confirm.literal)
else
markup.set_dyn_param(:"data-confirm", "<%= fquote(#{confirm}) %>")
end
end
if remote_target
# ajax link (link_to_remote)
# Add href to non-ajax method.
markup.set_dyn_param(:href, "<%= #{make_href(nil, options.merge(:update => false))} %>")
if true
# Use onclick with Ajax.
if confirm
markup.set_dyn_param(:onclick, "if(confirm(this.getAttribute(\"data-confirm\"))) {new Ajax.Request(\"<%= #{href} %>\", {asynchronous:true, evalScripts:true, method:\"#{http_method}\"});} return false;")
else
markup.set_dyn_param(:onclick, "new Ajax.Request(\"<%= #{href} %>\", {asynchronous:true, evalScripts:true, method:\"#{http_method}\"}); return false;")
end
else
#### FIXME: We need the 'update' parameter to trigger a js response for delete but we ignore
#### the content.
if remote_target.kind_of?(String)
# YUCK. We should have a way to have dom_ids that do not need
# us to look for remote_target !
remote_target = find_target(remote_target)
end
# Experimental new model for javascript actions.
# Works for 'swap' but needs more adaptations for 'edit' or other links
hash_params = []
(options[:query_params] || @params).each do |key, value|
next if [:update, :href, :eval, :text, :attr, :t, :host].include?(key)
case key
when :anchor
# Get anchor and force string interpolation
value = "%Q{#{get_anchor_name(value)}}"
when :publish
if value == 'true'
key = 'node[v_status]'
value = Zena::Status::Pub
else
next
end
when :encode_params, :format, :mode, :insert, :states
# Force string interpolation
value = "%Q{#{value}}"
else
if value.blank?
value = "''"
end
end
hash_params << "#{key.inspect} => #{value}"
end
if host = param(:host)
hash_params << ":host => %Q{#{host}}"
end
if !hash_params.blank?
query = RubyLess.translate(self, "{#{hash_params.join(', ')}}.to_json")
else
query = ''
end
dom_id, dom_prefix = get_dom_id(remote_target)
markup.set_dyn_param(:onclick, %Q{return Zena.#{http_method}("<%= #{dom_id} %>",<%= #{query} %>)})
end
else
markup.set_dyn_param(:href, "<%= #{href} %>")
if http_method != 'get' || confirm
markup.set_dyn_param(:onclick, "return Zena.m(this,#{http_method.inspect})")
end
end
# We wrap without callbacks (before_wrap, after_wrap) so that the link
# is used as raw text in these callbacks.
markup.wrap(text)
=begin
query_params = options[:query_params] || {}
default_text = options[:default_text]
params = {}
(options[:params] || @params).each do |k,v|
next if v.nil?
params[k] = v
end
opts = {}
if href = params.delete(:href)
if lnode = get_context_var('set_var', value) && stored.klass <= Node
# using stored node
else
lnode, klass = build_finder(:first, href, {})
return unless lnode
return parser_error("invalid class (#{klass})") unless klass.ancestors.include?(Node)
end
else
# obj
if node_class == Version
lnode = "#{node}.node"
opts[:lang] = "#{node}.lang"
elsif node.will_be?(Node)
lnode = node
else
lnode = @context[:previous_node]
end
end
if fmt = params.delete(:format)
if fmt == 'data'
opts[:format] = "#{node}.ext"
else
opts[:format] = fmt.inspect
end
end
if mode = params.delete(:mode)
opts[:mode] = mode.inspect
end
if anchor = params.delete(:anchor)
opts[:anchor] = anchor.inspect
end
if anchor_in = params.delete(:in)
finder, klass = build_finder(:first, anchor_in, {})
return unless finder
return parser_error("invalid class (#{klass})") unless klass.ancestors.include?(Node)
opts[:anchor_in] = finder
end
if @html_tag && @html_tag != 'a'
# FIXME: can we remove this ?
# html attributes do not belong to anchor
pre_space = ''
html_params = {}
else
html_params = get_html_params(params.merge(@html_tag_params), :link)
pre_space = @space_before || ''
@html_tag_done = true
end
(params.keys - [:style, :class, :id, :rel, :name, :anchor, :attr, :tattr, :trans, :text]).each do |k|
next if k.to_s =~ /if_|set_|\A_/
query_params[k] = params[k]
end
# TODO: merge these two query_params cleanup things into something cleaner.
else
# direct link
query_params.each do |k,v|
if k == :date
if v == 'current_date'
query_params[k] = current_date
elsif v =~ /\A\d/
query_params[k] = v.inspect
elsif v =~ /\[/
attribute, static = parse_attributes_in_value(v.gsub('"',''), :erb => false)
query_params[k] = "\"#{attribute}\""
else
query_params[k] = node_attribute(v)
end
else
attribute, static = parse_attributes_in_value(v.gsub('"',''), :erb => false)
query_params[k] = "\"#{attribute}\""
end
end
query_params.merge!(opts)
opts_str = ''
query_params.keys.sort {|a,b| a.to_s <=> b.to_s }.each do |k|
opts_str << ",:#{k.to_s.gsub(/[^a-z_A-Z_]/,'')}=>#{query_params[k]}"
end
pre_space + "#{text_for_link(default_text)}"
end
=end
end
protected
# Get default anchor name
def get_anchor_name(anchor_name)
if anchor_name == 'true'
if node.will_be?(Node)
'node#{id}'
elsif node.will_be?(Version)
'version#{node.id}_#{id}'
else
# ???
anchor_name
# force compilation with Node context. Why ?
#node_bak = @context[:node]
#@context[:node] = node(Node)
# anchor_name = ::RubyLess.translate_string(self, anchor_name)
#@context[:node] = node_bak
end
else
anchor_name
end
end
private
# Build the 'href' part of a link.
def make_href(remote_target = nil, opts = {})
anchor = @params[:anchor]
if anchor && !@params[:href]
# Link on same page
return ::RubyLess.translate(self, "%Q{##{get_anchor_name(anchor)}}")
end
if opts[:action] == 'edit' && remote_target
method = 'zafu_node_path'
elsif NODE_ACTIONS[opts[:action]]
method = "#{opts[:action]}_node_path"
elsif remote_target && opts[:action] != 'destroy'
method = 'zafu_node_path'
else
method = 'zen_path'
end
method_args = []
hash_params = []
if href = @params[:href]
method_args << href
elsif node.will_be?(Version)
method_args << "this.node"
hash_params << ":lang => this.lang"
else
method_args << '@node'
end
insert_ajax_args(remote_target, hash_params, opts[:action]) if remote_target
(opts[:query_params] || @params).each do |key, value|
next if [:update, :href, :eval, :text, :attr, :t, :host].include?(key)
case key
when :anchor
# Get anchor and force string interpolation
value = "%Q{#{get_anchor_name(value)}}"
when :publish
if value == 'true'
key = 'node[v_status]'
value = Zena::Status::Pub
else
next
end
when :encode_params, :format, :mode, :insert
# Force string interpolation
value = "%Q{#{value}}"
else
if value.blank?
value = "''"
end
end
hash_params << "#{key.inspect} => #{value}"
end
if host = param(:host)
hash_params << ":host => %Q{#{host}}"
end
unless hash_params.empty?
method_args << hash_params.join(', ')
end
method = "#{method}(#{method_args.join(', ')})"
::RubyLess.translate(self, method)
end
def insert_ajax_args(target, hash_params, action)
hash_params << ":s => start_id"
hash_params << ":link_id => this.link_id" if @context[:has_link_id] && node.will_be?(Node) && !node.list_context?
# FIXME: when we have proper markup.dyn_params[:id] support,
# we should not need this crap anymore.
case action
when 'edit'
# 'each' or 'block' target in parent hierarchy
is_list = ancestor(%w{each block}).method == 'each'
@insert_dom_id = %Q{"#{node.dom_id(:erb => false, :list => is_list)}"}
hash_params << ":dom_id => insert_dom_id"
hash_params << ":t_url => %Q{#{form_url(node.dom_prefix)}}"
# To enable link edit fix the following line:
# hash_params << "'node[link_id]' => link_id"
when 'unlink', 'destroy'
@insert_dom_id = %Q{"#{node.dom_id(:erb => false)}"}
hash_params << ":dom_id => insert_dom_id"
hash_params << ":t_url => %Q{#{template_url(node.dom_prefix)}}"
else #drop, #swap
if target == '_page'
# reload full page
hash_params << ":udom_id => '_page'"
return
elsif target.kind_of?(String)
# named target
if target_block = find_target(target)
target = target_block
else
out parser_error("Could not find target name '#{target}'.")
return nil
end
end
@insert_dom_id, dom_prefix = get_dom_id(target)
hash_params << ":dom_id => insert_dom_id"
hash_params << ":t_url => %Q{#{template_url(dom_prefix)}}"
end
end
#
def pagination_links
return parser_error("not in pagination scope") unless pagination_key = get_context_var('paginate', 'key')
page_direction = @params.delete(:page)
case page_direction
when 'previous', 'next'
current = get_context_var('paginate', 'current')
count = get_context_var('paginate', 'count')
prev_or_next = get_var_name('paginate', page_direction)
if page_direction == 'previous'
cond = "#{prev_or_next} = (#{current} > 1 ? #{current} - 1 : nil)"
else
cond = "#{prev_or_next} = (#{count} - #{current} > 0 ? #{current} + 1 : nil)"
end
# previous_page // next_page
set_context_var('set_var', "#{page_direction}_page",
RubyLess::TypedString.new(prev_or_next, :class => Number, :nil => true)
)
unless descendant('link')
# Do not wrap twice
link = {
:href => '@node',
:eval => "#{page_direction}_page",
pagination_key => "#{page_direction}_page",
}.merge(@params)
#
wrap_in_block :method => 'link', :params => link
end
out expand_if(cond)
when 'list'
node_count = get_context_var('paginate', 'nodes')
page_count = get_context_var('paginate', 'count')
curr_page = get_context_var('paginate', 'current')
page_number = get_var_name('paginate', 'page')
page_join = get_var_name('paginate', 'join')
page_name = get_var_name('paginate', 'page_name')
# give access to page_name
# FIXME: DOC
set_context_var('set_var', 'page_name', RubyLess::TypedString.new(page_name, String))
if @blocks == [] || (@blocks.size == 1 && !@blocks.first.kind_of?(String) && @blocks.first.method == 'else')
# We need to insert the default 'link' tag:
link = {}
@params.each do |k,v|
next if [:tag, :page, :join, :page_count].include?(k)
# transfer params
link[k] = v
end
tag = @params[:tag]
link[:html_tag] = tag if tag
link[:href] = '@node'
link[:eval] = 'page_name'
link[pagination_key.to_sym] = 'this'
#
add_block :method => 'link', :params => link
end
if !descendant('else')
else_tag = {:method => 'else', :text => "#{@markup.space_before}"}
else_tag[:tag] = tag if tag
add_block else_tag
# Clear cached descendants
@all_descendants = nil
end
out "<% page_numbers(#{curr_page}, #{page_count}, #{(@params[:join] || ' ').inspect}, #{@params[:page_count] ? @params[:page_count].to_i : 'nil'}) do |#{page_number}, #{page_join}, #{page_name}| %>"
out "<%= #{page_join} %>"
with_context(:node => node.move_to(page_number, Number)) do
out expand_if("#{page_number} != #{curr_page}")
end
out "<% end %>"
else
parser_error("unkown option for 'page' #{@params[:page].inspect} should be ('previous', 'next' or 'list')")
end
end
def text_for_link(default = nil)
if dynamic_blocks?
expand_with
else
method = get_attribute_or_eval(false)
if !method && (@params.keys & [:attr, :eval, :text, :t]) != []
out @errors.last
end
if method
if method.opts[:html_safe]
method.literal || "<%= #{method} %>"
else
method.literal ? ::ERB::Util.html_escape(method.literal) : "<%=h #{method} %>"
end
elsif default
default
elsif node.will_be?(Node)
"<%=h #{node(Node)}.prop['title'] %>"
elsif node.will_be?(Version)
"<%=h #{node(Version)}.node.prop['title'] %>"
elsif node.will_be?(Link)
"<%=h #{node(Link)}.name %>"
else
_('edit')
end
end
end
# Return the HTTP verb to use for the given action.
def http_method_from_action(action)
(NODE_ACTIONS[action] || {})[:method] || 'get'
end
end # ZafuMethods
end # Urls
end # Use
end # Zena