fix CSRF issues with tinymce and some ajax actions + begin to work on the roles feature powered by cancan (in progress, not stable)

This commit is contained in:
did 2011-06-25 09:25:31 -07:00
parent 33a29210ba
commit 09171555a7
32 changed files with 340 additions and 92 deletions

View File

@ -26,6 +26,7 @@ gem 'dragonfly', '~> 0.9.1'
gem 'rack-cache', :require => 'rack/cache'
gem 'custom_fields', '1.0.0.beta.19'
gem 'cancan'
gem 'fog', '0.8.2'
gem 'mimetype-fu'
gem 'actionmailer-with-request', :require => 'actionmailer_with_request'

View File

@ -62,6 +62,7 @@ GEM
highline (>= 1.6.1)
json (>= 1.4.6)
rest-client (>= 1.6.1)
cancan (1.6.0)
capybara (0.4.1.2)
celerity (>= 0.7.9)
culerity (>= 0.2.4)
@ -280,6 +281,7 @@ DEPENDENCIES
bson_ext (~> 1.3.0)
bushido
bushido_stub!
cancan
capybara
cucumber (= 0.8.5)
cucumber-rails

View File

@ -15,7 +15,7 @@ module Admin
end
def create
params[:asset] = { :name => params[:name], :source => params[:file] } if params[:file]
@asset = current_site.assets.build(:name => params[:name], :source => params[:file])
create! do |success, failure|
success.json do

View File

@ -9,11 +9,13 @@ module Admin
before_filter :require_site
load_and_authorize_resource
before_filter :validate_site_membership
before_filter :set_locale
helper_method :sections, :current_site_url, :site_url, :page_url
helper_method :sections, :current_site_url, :site_url, :page_url, :current_ability
# https://rails.lighthouseapp.com/projects/8994/tickets/1905-apphelpers-within-plugin-not-being-mixed-in
Dir[File.dirname(__FILE__) + "/../../helpers/**/*_helper.rb"].each do |file|
@ -26,8 +28,26 @@ module Admin
respond_to :html
rescue_from CanCan::AccessDenied do |exception|
puts "exception = #{exception.inspect}"
logger.debug "[CanCan::AccessDenied] #{exception.inspect}"
if request.xhr?
render :json => { :error => exception.message }
else
flash[:alert] = exception.message
redirect_to admin_pages_url
end
end
protected
def current_ability
@current_ability ||= Ability.new(current_admin, current_site)
end
def require_admin
authenticate_admin!
end

View File

@ -7,8 +7,11 @@ module Admin
respond_to :json, :only => :update
# before_filter :authorize
def index
@contents = @content_type.list_or_group_contents
authorize! :index, ContentInstance
end
def create

56
app/models/ability.rb Normal file
View File

@ -0,0 +1,56 @@
class Ability
include CanCan::Ability
ROLES = %w(admin author designer)
def initialize(account, site)
@account, @site = account, site
alias_action :index, :show, :edit, :update, :to => :touch
@membership = @site.memberships.where(:account_id => @account.id).first
return false if @membership.blank?
if @membership.admin?
setup_admin_permissions!
else
setup_default_permissions!
setup_designer_permissions! if @membership.designer?
setup_author_permissions! if @membership.author?
end
end
def setup_default_permissions!
cannot :manage, :all
end
def setup_author_permissions!
can :touch, [Page, ThemeAsset]
can :sort, Page
can :manage, [ContentInstance, Asset]
end
def setup_designer_permissions!
can :manage, Page
can :manage, ContentInstance
can :manage, ContentType
can :manage, ThemeAsset
can :import, Site
can :point, Site
can :manage, Membership
end
def setup_admin_permissions!
can :manage, :all
end
end

View File

@ -25,6 +25,8 @@ class Asset
## methods ##
alias :name :source_filename
def extname
return nil unless self.source?
File.extname(self.source_filename).gsub(/^\./, '')

View File

@ -6,7 +6,7 @@ module Extensions
def create_first_one(attributes)
site = self.new(attributes)
site.memberships.build :account => Account.first, :admin => true
site.memberships.build :account => Account.first, :role => 'admin'
site.save

View File

@ -3,7 +3,8 @@ class Membership
include Locomotive::Mongoid::Document
## fields ##
field :admin, :type => Boolean, :default => false
# field :admin, :type => Boolean, :default => false
field :role, :default => 'author'
## associations ##
referenced_in :account, :validate => false
@ -12,8 +13,17 @@ class Membership
## validations ##
validates_presence_of :account
## callbacks ##
before_save :define_role
## methods ##
Ability::ROLES.each do |_role|
define_method("#{_role}?") do
self.role == _role
end
end
def email; @email; end
def email=(email)
@ -36,4 +46,14 @@ class Membership
end
end
def ability
@ability ||= Ability.new(self.account, self.site)
end
protected
def define_role
self.role = Ability::ROLES.include?(role.downcase) ? role.downcase : Ability::ROLES.first
end
end

View File

@ -28,6 +28,7 @@ class Site
## behaviours ##
enable_subdomain_n_domains_if_multi_sites
accepts_nested_attributes_for :memberships
## methods ##

View File

@ -41,12 +41,27 @@
%button{ :class => 'button light add', :type => 'button' }
%span!= t('admin.buttons.new_item')
= f.foldable_inputs :name => :memberships, :class => 'memberships' do
- @site.memberships.each_with_index do |membership, index|
- account = membership.account
%li{ :class => "item #{'last' if index == @site.memberships.size - 1}" }
- if current_ability.can?(:touch, Membership)
= f.foldable_inputs :name => :memberships, :class => 'memberships off' do
= f.semantic_fields_for :memberships do |fm|
- membership, account = fm.object, fm.object.account
%li.item.membership{ :'data-role' => membership.role }
%strong= account.name
%em= account.email
- if account != current_admin
%em.email= account.email
- if current_admin != account && current_ability.can?(:touch, Membership)
.role
%em.editable= t("admin.memberships.roles.#{membership.role}")
= fm.select :role, Ability::ROLES.collect { |r| [t("admin.memberships.roles.#{r}"), r] }, :include_blank => false
%span.actions
= link_to image_tag('admin/form/icons/trash.png'), admin_membership_url(membership), :class => 'remove first', :confirm => t('admin.messages.confirm'), :method => :delete
= link_to image_tag('admin/form/icons/trash.png'), admin_membership_url(membership), :class => 'remove first', :confirm =>t('admin.messages.confirm'), :method => :delete
- else
.role
%em.locked= t("admin.memberships.roles.#{membership.role}")

View File

@ -29,7 +29,7 @@
—
%em {{kind_name}}
%em.editable {{kind_name}}
= select_tag '{{base_name}}[kind]', options_for_select(options_for_field_kind), :'data-field' => 'kind'

View File

@ -2,7 +2,9 @@
= include_javascripts :image_picker, :edit_page
= include_stylesheets :editable_elements, :fancybox
= f.foldable_inputs :name => :information do
- if can?(:manage, @page)
= f.foldable_inputs :name => :information do
= f.input :title
@ -11,13 +13,17 @@
= f.input :slug, :required => false, :hint => @page.slug.blank? ? ' ' : page_url(@page), :input_html => { :data_url => get_path_admin_pages_url, :disabled => @page.index? || @page.not_found? }, :wrapper_html => { :style => "#{'display: none' if @page.templatized?}; height: 50px" }
= render 'editable_elements', :page => @page
= f.foldable_inputs :name => :seo do
= f.input :seo_title
= f.input :meta_keywords
= f.input :meta_description
= f.foldable_inputs :name => :advanced_options do
- if can?(:manage, @page)
= f.foldable_inputs :name => :advanced_options do
= f.input :content_type_id, :as => :select, :collection => current_site.content_types.all.to_a, :include_blank => false, :wrapper_html => { :style => "#{'display: none' unless @page.templatized?}; height: 50px" }
@ -37,9 +43,8 @@
= f.input :redirect_url, :required => true, :wrapper_html => { :style => "#{'display: none' unless @page.redirect?}" }
= render 'editable_elements', :page => @page
= f.foldable_inputs :name => :raw_template do
= f.foldable_inputs :name => :raw_template do
= f.custom_input :value, :css => 'code full', :with_label => false do
= f.label :raw_template
%code{ :class => 'html' }

View File

@ -1,5 +1,8 @@
%li{ :id => "item-#{page.id}", :class => "#{'not-found' if page.not_found? } #{'templatized' if page.templatized?}"}
- with_children = !page.children.empty?
- children = can?(:manage, page) ? page.children : page.children.find_all { |p| !p.templatized? }
- with_children = !children.empty?
- if not page.index? and with_children
= image_tag 'admin/list/icons/node_closed.png', :class => 'toggler'
@ -10,9 +13,10 @@
%span!= t('.updated_at')
= l page.updated_at, :format => :short
- if not page.index? and not page.not_found?
- if !page.index_or_not_found? && can?(:manage, page)
= link_to image_tag('admin/list/icons/trash.png'), admin_page_url(page), :class => 'remove', :confirm => t('admin.messages.confirm'), :method => :delete
- if with_children
%ul{ :id => "folder-#{page._id}", :class => "folder depth-#{(page.depth || 0) + 1}", :data_url => sort_admin_page_url(page), :style => "display: #{cookies["folder-#{page._id}"] || 'block'}" }
= render page.children
= render children

View File

@ -9,7 +9,8 @@
- content_for :actions do
= render 'admin/shared/actions/contents'
- content_for :buttons do
- if can? :create, Page
- content_for :buttons do
= admin_button_tag :new, new_admin_page_url, :class => 'new'
%p!= t('.help')

View File

@ -1 +1,2 @@
= link_to content_tag(:em) + content_tag(:span, t('admin.content_types.index.new')), new_admin_content_type_url
- if can? :manage, ContentType
= link_to content_tag(:em) + content_tag(:span, t('admin.content_types.index.new')), new_admin_content_type_url

View File

@ -1,4 +1,5 @@
= admin_submenu_item 'pages', admin_pages_url do
- if can? :manage, @page
.header
%p= link_to t('admin.pages.index.new'), new_admin_page_url
.inner
@ -12,7 +13,10 @@
- each_content_type_menu_item do |content_type|
.header
%p= link_to t('admin.contents.index.new'), new_admin_content_url(content_type.slug)
- if can? :manage, content_type
%p.edit= link_to t('admin.contents.index.edit'), edit_admin_content_type_url(content_type)
.inner
%h2!= t('admin.contents.index.lastest_items')
%ul

View File

@ -138,10 +138,14 @@ en:
edit:
import: import
new_membership: add account
help: "The site name can be updated by clicking it."
help: "The site name can be updated by clicking it. To apply your changes, click on the \"Update\" button."
ask_for_name: "Please type the new site name"
memberships:
roles:
admin: Administrator
designer: Designer
author: Author
new:
title: New membership
help: "Please give the account email to add. If it does not exist, you will be redirected to the account creation form."
@ -153,7 +157,7 @@ en:
my_accounts:
edit:
help: "Your name can be updated by clicking it."
help: "Your name can be updated by clicking it. To apply your changes, click on the \"Update\" button."
new_site: new site
en: English
de: German

View File

@ -138,7 +138,7 @@ fr:
edit:
import: importer
new_membership: ajouter compte
help: "Le nom du site est modifiable en cliquant dessus."
help: "Le nom du site est modifiable en cliquant dessus. Pour appliquer votre modification, cliquez après sur le bouton \"Modifier\""
ask_for_name: "Veuillez entrer le nouveau nom"
memberships:
@ -153,7 +153,7 @@ fr:
my_accounts:
edit:
help: "Votre nom est modifiable en cliquant dessus."
help: "Votre nom est modifiable en cliquant dessus. Pour appliquer votre modification, cliquez après sur le bouton \"Modifier\""
new_site: nouveau site
en: en Anglais
de: en Allemand

View File

@ -31,9 +31,22 @@ x Has_one => group by in the select
x better hints:
x notify the user that after changing the page title, they still have to click "update" for the change to be saved
x created_by ASC => "Creation date ascending"
- cancan: authors / designers
- cancan: (authors / designers / admin)
x model
x ui
- controllers / views:
- page
- asset
- content type
- site
- account
- snippet
- theme asset
- better ui: increase text field length + refactor error message
- convert existing templates (the 2 of the themes section)
- bug heroku: unable to upload a new file
- bugs
- heroku: unable to upload a new file
- import
BACKLOG:

View File

@ -13,6 +13,22 @@ namespace :locomotive do
namespace :upgrade do
desc 'Set roles to the existing users'
task :set_roles => :environment do
Site.all.each do |site|
site.memberships.each do |membership|
if membership.attributes['admin'] == true
puts "...[#{site.name}] #{membership.account.name} has now the admin role"
membership.role = 'admin'
else
puts "...[#{site.name}] #{membership.account.name} has now the author role"
membership.role = 'author'
end
end
site.save
end
end
desc 'Remove asset collections and convert them into content types'
task :remove_asset_collections => :environment do
puts "Processing #{AssetCollection.count} asset collection(s)..."

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

View File

@ -24,6 +24,7 @@ $(document).ready(function() {
'update': function(event, ui) {
var params = $(this).sortable('serialize', { 'key': 'children[]' });
params += '&_method=put';
params += '&' + $('meta[name=csrf-param]').attr('content') + '=' + $('meta[name=csrf-token]').attr('content');
$.post($(this).attr('data_url'), params, function(data) {
var error = typeof(data.error) != 'undefined';

View File

@ -43,7 +43,7 @@
</div>
</div>
<div class="actions">
<a href="#" class="remove" data-confirm="{#locomedia_dlg.confirm}" data-method="delete" rel="nofollow">
<a href="#" class="remove" data-remote="true" data-confirm="{#locomedia_dlg.confirm}" data-method="delete" rel="nofollow">
<img src="/images/admin/list/icons/cross.png">
</a>
</div>

View File

@ -1 +1 @@
(function(){tinymce.create('tinymce.plugins.LocoMediaPlugin',{init:function(ed,url){ed.addCommand('locoMedia',function(){ed.windowManager.open({file:url+'/dialog.htm',width:645,height:650,inline:1},{plugin_url:url})});ed.addButton('locomedia',{title:'locomedia.image_desc',cmd:'locoMedia'})},getInfo:function(){return{longname:'Locomotive Media File',author:'Didier Lafforgue',authorurl:'http://www.locomotivecms.com',infourl:'http://www.locomotivecms.com',version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add('locomedia',tinymce.plugins.LocoMediaPlugin)})();
(function(){tinymce.create('tinymce.plugins.LocoMediaPlugin',{init:function(ed,url){ed.addCommand('locoMedia',function(){ed.windowManager.open({file:url+'/dialog.htm?7',width:645,height:650,inline:1},{plugin_url:url})});ed.addButton('locomedia',{title:'locomedia.image_desc',cmd:'locoMedia'})},getInfo:function(){return{longname:'Locomotive Media File',author:'Didier Lafforgue',authorurl:'http://www.locomotivecms.com',infourl:'http://www.locomotivecms.com',version:tinymce.majorVersion+"."+tinymce.minorVersion}}});tinymce.PluginManager.add('locomedia',tinymce.plugins.LocoMediaPlugin)})();

View File

@ -15,6 +15,13 @@ var MediafileDialog = {
init : function(ed) {
var self = this;
with(window.parent) {
var csrf_token = $('meta[name=csrf-token]').attr('content'),
csrf_param = $('meta[name=csrf-param]').attr('content');
}
$.fn.setCsrfSettings(csrf_token, csrf_param);
formElement = $(document.forms[0]);
listElement = formElement.find('ul');
@ -153,13 +160,6 @@ var MediafileDialog = {
asset.find('.actions a')
.attr('href', data.destroy_url)
.bind('click', function(e) {
if (confirm($(this).attr('data-confirm'))) {
self.showSpinner('destroying');
$(this).callRemote();
}
e.preventDefault(); e.stopPropagation();
})
.bind('ajax:success', function(event, data) {
self._destroyAsset(asset);
});

View File

@ -3,6 +3,12 @@ jQuery(function ($) {
csrf_param = $('meta[name=csrf-param]').attr('content');
$.fn.extend({
setCsrfSettings: function(token, param) {
csrf_token = token;
csrf_param = param;
},
/**
* Triggers a custom event on an element and returns the event result
* this is used to get around not being able to ensure callbacks are placed
@ -32,6 +38,10 @@ jQuery(function ($) {
} else {
if (el.triggerAndReturn('ajax:before')) {
var data = el.is('form') ? el.serializeArray() : [];
if (!el.is('form') && method != 'GET')
data.push({ 'name': csrf_param, 'value': csrf_token });
$.ajax({
url: url,
data: data,

View File

@ -46,4 +46,29 @@ $(document).ready(function() {
$('#header h1 a span.ui-selectmenu-status').html(value);
$('#site-selector-menu li.ui-selectmenu-item-selected a').html(value);
}, []);
// account roles
$('.membership .role em.editable').click(function() {
$(this).hide();
$(this).next().show();
});
$('.membership .role select').each(function() {
var select = $(this);
select.hover(function() {
clearTimeout($.data(select, 'timer'));
},
function() {
$.data(select, 'timer', setTimeout(function() {
select.hide();
select.prev().show();
}, 1000));
}).change(function() {
select.hide().prev()
.show()
.html(select[0].options[select[0].options.selectedIndex].text);
});
}).hide();
});

View File

@ -24,6 +24,7 @@ form.formtastic legend span {
color: #1e1f26;
font-size: 0.7em;
padding: 4px 0 0 20px;
text-shadow: #fff 0px 1px;
}
form.formtastic legend span small {
@ -246,6 +247,28 @@ form.formtastic fieldset ol li.item em {
color: #757575;
}
form.formtastic fieldset ol li em.editable {
display: inline-block;
position: relative;
top: -1px;
color: #8b8d9a;
font-size: 0.9em;
font-style: italic;
margin-left: 3px;
border: 1px solid transparent;
padding: 2px 5px;
height: 18px;
line-height: 16px;
}
form.formtastic fieldset ol li em.editable:hover {
background: #fffbe5;
border: 1px dotted #efe4a5;
cursor: pointer;
color: #17171D;
font-weight: bold;
}
form.formtastic fieldset ol li.item span.actions {
position: absolute;
top: 5px;
@ -281,27 +304,6 @@ form.formtastic fieldset.editable-list ol li.added select {
top: -1px;
}
form.formtastic fieldset.editable-list ol li.added em {
display: inline-block;
position: relative;
top: -1px;
color: #8b8d9a;
font-size: 0.9em;
font-style: italic;
margin-left: 3px;
border: 1px solid transparent;
padding: 2px 5px;
height: 18px;
line-height: 16px;
}
form.formtastic fieldset.editable-list ol li.added em:hover {
background: #fffbe5;
border: 1px dotted #efe4a5;
cursor: pointer;
color: #17171D;
font-weight: bold;
}
form.formtastic fieldset.editable-list ol li.added select,
form.formtastic fieldset.editable-list ol li.added em {
width: 150px;
@ -552,6 +554,42 @@ form.formtastic fieldset.email li.full input {
margin-left: 20px;
}
form.formtastic fieldset.memberships ol li .role {
position: absolute;
top: 2px;
right: 30px;
width: 170px;
text-align: left;
}
form.formtastic fieldset.memberships ol li .role em {
display: inline-block;
position: relative;
top: -1px;
color: #757575;
font-size: 0.8em;
padding: 2px 5px 2px 17px;
height: 18px;
line-height: 16px;
margin-left: 0px;
}
form.formtastic fieldset.memberships ol li .role em.locked {
background: transparent url(/images/admin/icons/membership_lock.png) no-repeat 1px 3px;
}
form.formtastic fieldset.memberships ol li .role em.editable {
background: transparent url(/images/admin/icons/membership_edit.png) no-repeat left 3px;
}
form.formtastic fieldset.memberships ol li .role em.editable { font-style: normal; font-size: 0.8em; }
form.formtastic fieldset.memberships ol li .role em.editable:hover { color: #000; font-style: normal; background: #fffbe5; padding-left: 5px; }
form.formtastic fieldset.memberships ol li select {
position: relative;
top: -1px;
}
/* ___ assets ___ */
.selector {

View File

@ -154,17 +154,19 @@
text-decoration: none; }
#submenu .popup a:hover {
text-decoration: underline; }
#submenu .popup .header {
border-bottom: 1px dotted #bbbbbd;
padding-bottom: 6px;
margin: 0px 16px; }
#submenu .popup .inner {
padding: 8px 16px; }
#submenu .popup h2 {
font-size: 0.7em;
font-weight: bold;
color: #1e1f26;
border-top: 1px dotted #bbbbbd;
padding-top: 6px;
margin-bottom: 0px; }
#submenu .popup p {
margin: 0px 15px;
margin: 0px;
padding: 10px 0 0 0px; }
#submenu .popup p a {
font-size: 0.8em;

View File

@ -164,19 +164,23 @@
&:hover { text-decoration: underline; }
}
.header {
border-bottom: 1px dotted #bbbbbd;
padding-bottom: 6px;
margin: 0px 16px;
}
.inner { padding: 8px 16px; }
h2 {
font-size: 0.7em;
font-weight: bold;
color: #1e1f26;
border-top: 1px dotted #bbbbbd;
padding-top: 6px;
margin-bottom: 0px;
}
p {
margin: 0px 15px;
margin: 0px;
padding: 10px 0 0 0px;
a {