乐者为王

Do one thing, and do it well.

使用响应式设计改造电子书网站

因为要在其它设备上测试响应式设计的效果,需要先对开发环境做些简单的配置。

启动服务器时需加上本机的IP地址和访问端口(通常是80):

1
rails s -b 192.168.0.100 -p 80

配置Windows系统的内置防火墙,开启80端口以供其它设备访问:

1
netsh advfirewall firewall add rule name="Open Port 80" dir=in action=allow protocol=TCP localport=80

查看和删除防火墙规则的命令:

1
2
netsh advfirewall firewall delete rule name="Open Port 80" protocol=TCP localport=80
netsh advfirewall firewall show rule name="Open Port 80"

响应式设计的优缺点就不多说了,已经有太多的文章讲过这些。这里主要讲如何使用响应式设计改造现有的电子书网站。

首先,需要使用viewport标签设置屏幕宽度为设备宽度,使网站内容可以适应响应式变化。

1
<meta name="viewport" content="width=device-width, initial-scale=1" />

因为这是一个非常简陋,并且以内容为主的网站。在上述设置后几乎不用再修改就能适应不同屏幕分辨率下的信息呈现,除了导航栏因为导航选项较多,在手机等设备上打开会出现后面部分选项被挤下去。抽屉样式的导航是解决这个问题的好方法。在网上找到一个横向导航栏切换成抽屉式导航栏的库jQuery PageSlide,它使用图片实现三明治图标,所以对它做了些修改,用标签来实现:

1
2
3
<ul class="burger">
  <li></li><li></li><li></li>
</ul>

对应的CSS代码如下:

1
2
3
4
5
6
7
8
9
.burger {
  li {
    height: 4px;
    width: 30px;
    background: #000;
    border-radius: 3px;
    margin: 5px 0;
  }
}

要实现响应式的话,只要让它在正常情况下隐藏,屏幕变小时显现即可:

1
2
3
4
5
6
7
8
9
.burger {
  display: none;
}

@media screen and (max-width: 480px) {
  .burger {
    display: block;
  }
}

还需要在application.html.erb的底部开启点击三明治图标时的响应事件:

1
2
3
4
<%= javascript_include_tag "jquery.pageslide", "data-turbolinks-track" => true %>
<script>
  $(".burger").pageslide();
</script>

导航条的CSS也需要做些修改,使之能在屏幕变小时转换成抽屉式导航条:

1
2
3
4
5
@media screen and (max-width: 480px) {
  nav ul {
    display: none;
  }
}

至此,网站的响应式改造就基本完成了。如果你觉得对你有所帮助,请将此文发送给你的朋友,如果你还有更好的建议也可以在下面的评论中分享你的经验。

使用jQuery、Rails 4和Paperclip进行多文件上传

英文原文:https://5minutenpause.com/blog/2013/09/04/multiple-file-upload-with-jquery-rails-4-and-paperclip/

在最近的项目中,我需要一个方便的文件上传器。我想要多文件上传和进度条。我想要它们都能与Bootstrap一起工作。jQuery File Upload能够满足这些需求。初看上去它似乎不容易使用,在研究后我发现Ryan Bates做过一个关于jQuery文件上传railscast。不幸的是,这个railscast使用了Rails 3和旧的jQuery版本。所以我必须调整它。涉及这个主题的其它博文都开始于2012年甚至更早,在这里是新的Rails 4版本。

我们不是生活在真空中,所以会经常性地使用已有的想法(一切都是混合),因此我没有试图去提出一个完全原创的解决方案,下面我的实现是基于Ryans的工作。

更新:我为这篇文章创建了一个GitHub仓库。你可以将其用作本文中我使用的所有内容的完整工作副本。我使用这些提交来跟踪博客文章,以便你可以使用它们“重播”我的实现。如果有任何问题,请在GitHub打开一个issue,并在那里询问,或者就在本文后面发布你的评论。谢谢。

更新2:Paul Walker在评论中指出了如何解决Turbolinks的问题。如果你也有这种情况,请看他对Turbolinks的评论

更新3:如果你想知道如何在后台完成图片处理,请参阅我的这个主题的新帖子。我会给你展示如何使用Rails 4.2的Active Job和Delayed Job来实现后台处理。

Gem和资源文件

我们从Gemfile开始,添加jquery-fileupload-rails这个gem。

1
gem 'jquery-fileupload-rails'

使用bundle install安装所有gem。

安装后,你需要在你的application.js中引入以下这些文件:

1
2
3
4
5
//= require jquery
//= require jquery_ujs
//= require jquery-fileupload/basic
//= require jquery-fileupload/vendor/tmpl
//= require_tree .

我们在这里使用基本版本,并且包含jquery-fileupload/vendor/tmpl,所以我们可以选择渲染我们自己的模板。

视图

我们有个表单用于上传文件,并将JavaScript模板包含在文件的底部。有件事要注意:模板脚本必须是没有换行或空格的单行程序。否则jQuery会抱怨:Uncaught Syntax error, unrecognized expression: [object Object]。另一个解决方案是使用$.parseHTML();。接下来我会给你展示如何在uploads.js.coffee中做到这点。

1
2
3
4
5
6
7
8
9
10
11
12
<%= form_for Upload.new, :url => uploads_path, html: { multipart: true } do |f| %>
  <%= f.label :uploaded_file, t('.upload_new_file') %>
  <%= f.file_field :uploaded_file, multiple: true, name: 'upload[uploaded_file]' %>
  <%= f.submit t(:save), class: 'btn' %>
<% end %>

<% # jquery upload template # %>
<script id="template-upload" type="text/x-tmpl">
  <div class="upload">
    { %=o.name % }<div class="progress"><div class="bar" style="width: 0%"></div></div>
  </div>
</script>

因为要返回JavaScript脚本,所以:create成功后要渲染的文件如下:

1
2
3
4
5
<% if @upload.new_record? %>
  alert('Failed');
<% else %>
  $('ul.thumbnails').append("<%=j render partial: 'photosets/upload', locals: { upload: @upload } %>");
<% end %>

当你点击提交以上传你的图片时,实际发生的是你上传的每张图片都有多次提交。现在我们需要处理这些提交。我们在CoffeeScript文件里面做这些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
jQuery ->
  $('#upload_uploaded_file').attr('name','upload[uploaded_file]')
  $('#new_upload').fileupload
    dataType: 'script'
    add: (e, data) ->
      types = /(\.|\/)(gif|jpe?g|png|mov|mpeg|mpeg4|avi)$/i
      file = data.files[0]
      if types.test(file.type) || types.test(file.name)
        data.context = $(tmpl("template-upload", file))
        $('#new_upload').append(data.context)
        data.submit()
      else
        alert("#{file.name} is not a gif, jpg or png image file")
    progress: (e, data) ->
      if data.context
        progress = parseInt(data.loaded / data.total * 100, 10)
        data.context.find('.bar').css('width', progress + '%')

这里我们检查文件的类型是图片还是电影。否则,我们将向用户显示不允许该文件的警告。如果文件被允许,我们使用文件的数据渲染模板,并将其附加到我们的图片列表中(这里没有显示代码——你可以很容易地弄明白)。然后我们提交文件进行实际上传并保存到数据库。此外,我们显示每个上传文件的进度条。

upload.js.coffee中的第2行用于将upload_file的名称从数组改为单个上传(从upload[uploaded_file][]upload[uploaded_file])。否则,上传数组会使Paperclip抛出错误Paperclip::AdapterRegistry::NoHandlerError。你可以使用file_field的name属性设置值,但那对我来说无法可靠地工作。

我在之前说过,你必须避免模板中的换行符。如果你将第9行改为:

1
data.context = $($.parseHTML($(tmpl("template-upload", file)))[1])

应该可以保持你的换行。我在Stack Overflow发现这个,但没有尝试过。但它看起来应该没问题。

控制器动作很简单:

1
2
3
def create
  @upload = Upload.create(upload_params)
end

这是个简单的实现。你可以做的更多。仔细看看jQuery File Upload的文档。如果你有任何问题,可以在twitter或app.net上询问我,我很乐意为你提供帮助。谢谢阅读。

Rails的不安全默认配置——需要知道的13个安全陷阱

英文原文:http://blog.codeclimate.com/blog/2013/03/27/rails-insecure-defaults/

安全的默认配置是构建安全系统的关键。如果开发者必须采取明确的行动来执行安全行为,即使是有经验的开发者最终也会忘记这样做。为此,安全专家说:

不安全的默认配置是不安全的。

Rails作为相对安全的Web框架实至名归。对于许多常见的攻击它都有开箱即用的防护:跨站脚本(XSS)、跨站请求伪造(CSRF)和SQL注入。Rails的核心成员知识渊博,真正关心安全。

然而,有些地方的默认行为可以做得更安全。这篇文章探讨了在Rails 4中修复的Rails 3中的潜在安全性问题,以及那些仍存在危险的安全性问题。我希望这篇文章可以帮助你保护自己的应用程序,同时启发Rails本身的修改。

Rails 3的问题

让我们从主干中已修复的某些Rails 3的问题开始。对于这些问题的解决,Rails团队值得称赞,但它们却毫无价值,因为许多应用程序在未来很多年里仍将运行在Rails 2和3上。

1. 通过有漏洞的#match路由进行CSRF攻击

这里是直接取自于Rails 3生成的config/routes.rb文件的一个示例:

1
2
3
4
5
6
7
WebStore::Application.routes.draw do
  # Sample of named route:
  match 'products/:id/purchase' => 'catalog#purchase',
    :as => :purchase
  # This route can be invoked with
  # purchase_url(:id => product.id)
end

这样做的后果是,对于任何HTTP动词(GET、POST等),/products/:id/purchase路径都将路由到CatalogController#purchase方法。问题是Rails的跨站请求伪造(CSRF)防护不适用于GET请求。你可以在实施CSRF防护的方法中看到这点:

1
2
3
4
5
6
7
8
def verified_request?
  !protect_against_forgery? ||
  request.get? ||
  form_authenticity_token ==
    params[request_forgery_protection_token] ||
  form_authenticity_token ==
    request.headers['X-CSRF-Token']
end

第2行短路CSRF检查:这意味着如果request.get?为true,请求会被认为是“验证过的”,并且CSRF检查将被跳过。事实上,在Rails的源代码中,该方法前面就有注释说:

Get应该是安全和幂等的。

在你的应用程序中,你可能总是使用POST向/products/:id/purchase提出请求。但是由于路由器允许GET请求,对于通过#match帮助器路由的任何方法,攻击者都可以轻松地绕过CSRF防护。如果你的应用程序使用旧的通配符路由(不推荐),则CSRF防护完全无效。

最佳实践:不要对不安全的动作使用GET。不要使用#match来添加路由(而是使用#post、#put等)。确保没有通配符路由。

修复:当使用#match添加路由时,Rails现在需要你指定明确的HTTP动词或via: :all。生成的config/routes.rb不再包含已注释掉的#match路由。(通配符路由也已被删除。)

2. 格式验证中的正则表达式锚点

考虑以下验证:

1
validates_format_of :name, with: /^[a-z ]+$/i

这段代码有个不易察觉的缺陷。开发者可能想强制整个名字属性仅由字母和空格组成。然而相反的是,这只会强制名字中至少有一行由字母和空格组成。正则表达式匹配的一些示例可以使这个缺陷更加清晰:

1
2
3
4
5
6
7
8
>> /^[a-z ]+$/i =~ "Joe User"
=> 0 # Match

>> /^[a-z ]+$/i =~ " '); -- foo"
=> nil # No match

>> /^[a-z ]+$/i =~ "a\n '); -- foo"
=> 0 # Match

开发者应该使用\A(字符串的开始)和\z(字符串的结尾)锚点替代^(行的开始)和$(行的结尾)。正确的代码是:

1
validates_format_of :name, with: /\A[a-z ]+\z/i

你可以认为开发者有错,而你是对的。然而,正则表达式锚点的行为不一定是明显的,特别是对于没有考虑多行值的开发者。(可能该属性仅被暴露在文本输入字段,而不是文本区域。)

Rails在拯救开发者方面做得不错,这正是Rails 4所做的工作。

最佳实践:尽可能使用\A和\z来锚定正则表达式,而不是^和$。

修复:Rails 4为validates_format_of引入了一个多行选项。如果你的正则表达式使用^和$而不是\A和\z进行锚定,并且没有传递multiline: true,则Rails将引发异常。这是创建更安全的默认行为的一个很好的示例,同时仍然提供控制以在必要的地方覆盖它。

3. 点击劫持

点击劫持或“用户界面伪装攻击”涉及在不可见的页帧中呈现目标站点,并诱骗受害者在点击时执行出乎意料的动作。如果一个站点很容易被点击劫持攻击,攻击者可能会诱骗用户执行不必要的动作,例如单击购买,在Twitter上跟随某人,或更改他们的隐私设置。

为防御点击劫持攻击,站点必须阻止自己被呈现在frame或不受控制的站点的iframe中。较老的浏览器需要丑陋的“页帧破解”JavaScript,但现代浏览器支持X-Frame-Options HTTP报头,它告知浏览器是否允许站点被页帧。这个报头很容易包含,也不可能破坏大多数的网站,所以Rails应该默认包含它。

最佳实践:使用Twitter的secure_headers添加一个X-Frame-Options报头,其值为SAMEORIGIN或DENY。

修复:默认情况下,Rails 4现在发送带有SAMEORIGIN值的X-Frame-Options报头:

1
X-Frame-Options: SAMEORIGIN

这告诉浏览器你的应用程序只能由源自同一个域的页面构成。

4. 用户可读的会话

默认的Rails 3会话存储使用已签名的未加密Cookie。虽然这样可以保护会话免遭篡改,但攻击者可以很容易对会话Cookie的内容进行解码:

1
2
3
4
5
6
7
8
9
10
11
session_cookie = <<-STR.strip.gsub(/\n/, '')
BAh7CEkiD3Nlc3Npb25faWQGOgZFRkkiJTkwYThmZmQ3Zm
dAY7AEZJIgtzZWtyaXQGO…--4c50026d340abf222…
STR

Marshal.load(Base64.decode64(session_cookie.split("--")[0]))
# => {
#   "session_id"  => "90a8f...",
#   "_csrf_token" => "iUoXA...",
#   "secret"      => "sekrit"
# }

在会话中存储任何敏感信息是不安全的。希望这是众所周知的,但即使用户的会话不包含敏感数据,它仍然可能会产生风险。通过解码会话数据,攻击者可以获得在攻击中可以利用的有关应用程序内部的有用信息。例如,可以得知哪个认证系统正在使用(Authlogic、Devise等)。

虽然这不会自行创建漏洞,但它可以帮助攻击者。关于应用程序如何工作的任何信息都可以被用于磨蚀攻击,并且在某些情况下,可以避免触发那些会给开发者提供正在被攻击的早期警告的异常或者绊网。

用户可读会话违反了最低权限原则,因为即使会话数据必须传递给访问者的浏览器,访问者也无需能够读取数据。

最佳实践:不要将任何你不希望攻击者访问的信息放入会话中。

修复:Rails 4将默认会话存储更改为加密的。没有解密密钥,用户在客户端将不再能够解码会话的内容。

尚未解决的问题

这篇文章的剩余部分讨论Rails在发布时仍然存在的安全风险。希望至少有些会被修复,如果是这样,我将更新这篇文章。

1. 详细的服务器报头

默认的Rails服务器是WEBrick(Ruby标准库的一部分),即使在生产中很少运行WEBrick。默认情况下,WEBrick对每个HTTP响应都返回一个详细的服务器报头:

1
2
3
HTTP/1.1 200 OK
# ...
Server: WEBrick/1.3.1 (Ruby/1.9.3/2012-04-20)

看看WEBrick的源代码,你可以看到报头是由几个关键信息组成的:

1
2
"WEBrick/#{WEBrick::VERSION} " +
"(Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})",

这暴露了WEBrick的版本,以及正在运行的特定的Ruby补丁级别(因为发布日期对应补丁级别)。有了这些信息,扫描工具可以更有效地针对你的服务器,攻击者可以定制其攻击有效载荷。

最佳实践:避免在生产中运行WEBrick。外面有更好的服务器,像Passenger、Unicorn、Thin和Puma。

修复:虽然此问题的根源在于WEBrick的源代码,但Rails应该将WEBrick配置为使用不那么详细的服务器报头。只打印“Ruby”似乎是个不错的选择。

2. 绑定到0.0.0.0

如果你启动一个Rails服务器,你将看到如下信息:

1
2
3
$ ./script/rails server -e production
=> Booting WEBrick
=> Rails 3.2.12 application starting in production on http://0.0.0.0:3000

Rails绑定在0.0.0.0(所有网络接口)而不是127.0.0.1(仅限本地接口)。这可能在开发和生产环境中产生安全风险。

在开发模式下,Rails不是安全的(例如,它呈现用于诊断的500页面)。另外,开发者可以加载生产数据和测试数据的混合(例如,username:admin/password:admin)。在旧金山咖啡店扫描Web服务器的端口3000可能会得到很好的目标。

在生产中,Rails应该运行在代理后面。没有代理,IP欺骗攻击就会经常发生。但是如果Rails绑定在0.0.0.0上,根据部署配置可以通过直接攻击Rails来轻松绕过代理。

因此,在所有Rails环境中,绑定到127.0.0.1是比0.0.0.0更安全的默认配置。

最佳实践:确保你的Web服务器进程在生产中绑定最小的接口集。避免在笔记本电脑上加载生产数据进行调试。如果你必须这样做,请加载最小的数据集,并在不需要时立即将其删除。

修复:Rails已经提供了--binding选项来更改服务器侦听的IP地址。默认值应该从0.0.0.0更改为127.0.0.1。需要在生产中绑定到其它接口的开发者可以在部署配置中进行更改。

3. 版本化的保密令牌

当使用rails new创建时,每个Rails应用程序都会在config/initializers/secret_token.rb中获取一个长的、随机生成的保密令牌。它看起来像这样:

1
WebStore::Application.config.secret_token = '4f06a7a…72489780f'

由于保密令牌是Rails自动创建的,所以大多数开发者不会去考虑它。但这个保密令牌就像你的应用程序的根密钥。如果你有保密令牌,伪造会话和提升权限就非常简单。它是要保护的敏感数据最关键的部分之一。加密其实就是你的密钥管理最佳实践。

不幸的是,Rails在处理这些保密令牌时没有达到预期效果。secret_token.rb文件最后会被检入到版本控制中,然后复制到GitHub、CI服务器和每个开发者的笔记本电脑。

最佳实践:在每个环境中使用不同的保密令牌。通过ENV变量注入应用程序。或者,在部署期间符号链接生产保密令牌。

修复:Rails至少应该默认使用.gitignore忽略config/initializers/secret_token.rb文件。当部署或更改初始化程序以使用ENV变量(例如Heroku)时,开发者可以符号链接生产令牌。

我会进一步提出Rails为保密令牌创建的存储机制。有许多库提供涉及将保密令牌检入到初始化程序的安装指令,这是个坏的实践。与此同时,至少有两个流行的策略来处理这个问题:ENV变量和符号链接的初始化程序。

Rails给开发者提供了一个简单的API来管理保密令牌,并且具有可交换的后端(如缓存存储和会话存储)。

4. 在SQL语句中记录值

Rails提供的config.filter_parameters是用来防止像密码这样的敏感信息累积在生产日志文件中的一种有用的方法。但它并不影响在SQL语句中值的记录:

1
2
3
4
5
6
7
8
9
10
Started POST "/users" for 127.0.0.1 at 2013-03-12 14:26:28 -0400
Processing by UsersController#create as HTML
  Parameters: {"utf8"=>"✓œ“", "authenticity_token"=>"...",
  "user"=>{"name"=>"Name", "password"=>"[FILTERED]"}, "commit"=>"Create User"}
  SQL (7.2ms)  INSERT INTO "users" ("created_at", "name", "password_digest",
  "updated_at") VALUES (?, ?, ?, ?)  [["created_at",
  Tue, 12 Mar 2013 18:26:28 UTC +00:00], ["name", "Name"], ["password_digest",
  "$2a$10$r/XGSY9zJr62IpedC1m4Jes8slRRNn8tkikn5.0kE2izKNMlPsqvC"], ["updated_at",
  Tue, 12 Mar 2013 18:26:28 UTC +00:00]]
Completed 302 Found in 91ms (ActiveRecord: 8.8ms)

在生产模式(info)中的默认Rails日志级别不会记录SQL语句。这里的风险在于,有时开发者会在调试时暂时提高生产中的日志级别。在这期间,应用程序可能会将敏感数据写入日志文件,然后保存在服务器上很长时间。获得读取服务器上的文件的访问权限的攻击者可以使用简单的grep查找数据。

最佳实践:知道在生产日志级别什么被记录。如果临时增加日志级别,导致敏感数据被记录,在不需要时要立即删除该数据。

修复:Rails可以将config.filter_parameters选项改成类似于config.filter_logs,并将其应用于参数和SQL语句。在所有情况下都正确过滤SQL语句是不可能的(因为它需要一个SQL解析器),但对于标准的插入和更新它可能是个80/20解决方案。

或者,如果包含对过滤值的引用,则Rails可以修改整个SQL语句(例如,修改包含“password”的所有语句),至少在生产模式下。

5. 离线重定向

许多应用程序包含需要根据上下文将用户发送到不同位置的控制器动作。最常见的示例是SessionsController,它将新验证的用户引导到其预期的目标网址或者默认的目标网址:

1
2
3
4
5
6
7
8
9
10
class SignupsController < ApplicationController
  def create
    # ...
    if params[:destination].present?
      redirect_to params[:destination]
    else
      redirect_to dashboard_path
    end
  end
end

这会产生风险,攻击者可以构建一个URL,导致不知情的用户在登录后被发送到恶意站点:

1
https://example.com/sessions/new?destination=http://evil.com/

无效的重定向可用于钓鱼式攻击,或者可能会损害用户对你的信任,因为你似乎将他们发送到恶意网站。即使是警惕的用户也可能不会在首个页面加载后检查网址栏以确保他们不被钓鱼。这个问题是足够严重的,已经成为最新版的OWASP十大应用安全威胁。

最佳实践:将散列传递到#redirect_to时,使用only_path: true选项将重定向限制为当前主机:

1
redirect_to params.merge(only_path: true)

当传递字符串时,你可以解析它来提取路径:

1
redirect_to URI.parse(params[:destination]).path

修复:默认情况下,Rails应该只允许同个域内(或白名单)的重定向。对于需要外部重定向的罕见情况,应该要求开发者将external: true选项传递给redirect_to,以便选择更危险的行为。

许多开发者没有意识到link_to帮助器的HREF属性可被用于注入JavaScript。这里是不安全代码的示例:

1
<%= link_to "Homepage", user.homepage_url %>

假设用户可以通过更新其画像来设置他们的homepage_url的值,这会产生XSS的风险。如下:

1
user.homepage_url = "javascript:alert('hello')"

将生成这样的HTML:

1
<a href="javascript:alert('hello')">Homepage</a>

单击链接将执行攻击者提供的脚本。Rails的XSS保护不会阻止这种情况。在社区迁移到更不唐突的JavaScript技术之前,这曾经是无法避免且常见的,但现在是个残留的弱点。

最佳实践:避免在HREF中使用不受信任的输入。当你必须允许用户控制HREF时,首先通过URI.parse分析输入,然后对协议和主机做完整性检测。

修复:默认情况下,Rails应该只允许路径、HTTP、HTTPS和mailto:href等值在link_to帮助器中。开发者必须通过传递选项给link_to帮助器以选择不安全的行为,或者link_to可能直接不支持这些,开发者需要手工制作他们的链接。

7. SQL注入

Rails在防止常见的SQL注入(SQLi)攻击方面做得比较好,所以开发者可能会认为Rails对SQLi是免疫的。当然不是这样。假设开发者需要根据参数完成orders表中的小计或总计。他们可能写:

1
Order.pluck(params[:column])

这样做是不安全的。显然,用户现在可以操纵应用程序从他们希望的orders表中检索任何数据列。然而,不太明显的是,攻击者还可以从其它表中提取值。例如:

1
2
params[:column] = "password FROM users--"
Order.pluck(params[:column])

将会变成:

1
SELECT password FROM users-- FROM "orders"

同样,#calculate的column_name属性实际上接受任意SQL:

1
2
params[:column] = "age) FROM users WHERE name = 'Bob'; --"
Order.calculate(:sum, params[:column])

将会变成:

1
SELECT SUM(age) FROM users WHERE name = 'Bob'; --) AS sum_id FROM "orders"

控制#calculate方法的column_name属性允许攻击者从任意表中的任意列中提取特定值。

rails-sqli.org详细介绍了哪些ActiveRecord方法和选项允许SQL,且带有它们如何被攻击的示例。

最佳实践:了解你使用的API以及它们在什么情况下可能允许比你预期的更危险的操作。使用最安全的API,和预期输入的白名单。

修复:这个问题难以批量解决,因为正确的解决方案因语境而异。一般来说,ActiveRecord API应该只允许常用的SQL片段。名为column_name的方法参数只能接受列名。可以为需要更多控制的开发者提供替代API。

感谢Twitter的Justin Collins撰写rails-sqli.org,它使我意识到了这个问题。

8. YAML反序列化

如同许多Ruby开发者在早期学到的,使用YAML反序列化不可信数据与eval一样不安全。已经有很多基于YAML攻击的文章,所以我不会在这里炒冷饭,但总而言之,如果攻击者可以注入一个YAML有效载荷,他们可以在服务器上执行任意代码。应用程序不需要做任何事情,只要加载YAML就容易易受攻击。

尽管Rails已经打过补丁,以避免解析在HTTP请求中发送给服务器的YAML,但它仍然使用YAML作为#serialize功能的默认序列化格式,就像新的#store功能(它本身就是围绕#serialize的简单包装)那样。危险代码看起来像这样:

1
2
3
4
5
6
class User < ActiveRecord::Base
  # ...
  serialize :preferences

  store :theme, accessors: [ :color, :bgcolor ]
end

大多数Rails开发者不会喜欢在数据库中存储任意Ruby代码,然后在加载记录时对其进行解释执行,按照这种方法,它与使用YAML反序列化功能相当。当存储的数据不包含任意Ruby对象时,它违反了最低权限原则。允许编写数据库中的值的漏洞可以被作为跳板以控制整个服务器。

我特别担心YAML的使用,因为它看起来安全但实际上危险。在远程代码执行(RCE)漏洞暴露之前,YAML格式已经被数以百计的熟练开发者查看多年。虽然这是Ruby社区的头等大事,但明年接手Rails的新开发者将不会经历YAML RCE的惨败。

最佳实践:使用JSON序列化格式而不是因为#serialize和#store使用YAML:

1
2
3
4
class User < ActiveRecord::Base
  serialize :preferences, JSON
  store :theme, accessors: [ :color, :bgcolor ], coder: JSON
end

修复:Rails应该将ActiveRecord的默认序列化格式从YAML切换到JSON。YAML行为应该通过选择加入或者提取到可选的Gem中。

9. 批量赋值

Rails 4从使用attr_accessible处理批量赋值漏洞切换到strong_parameters方法。params对象现在是ActionController::Parameters的实例。strong_parameters检查批量赋值中被使用的Parameters实例是否是“permitted”——开发者已经具体指出了哪些键(和值类型)是预期的。

一般来说,这是个积极的变化,但它确实引入了attr_accessible世界中不存在的一个新的攻击方向。考虑这个示例:

1
2
3
4
params = { user: { admin: true }.to_json }
# => {:user=>"{\"admin\":true}"}

@user = User.new(JSON.parse(params[:user]))

JSON.parse返回一个普通的Ruby Hash,而不是ActionController::Parameters的一个实例。使用strong_parameters的默认行为是允许Hash的实例通过批量赋值来设置任何模型属性。如果在访问ActiveRecord模型时使用Sinatra应用程序中的params会发生同样的问题——Sinatra不会把Hash包装成ActionController::Parameters的实例。

最佳实践:当把ActiveRecord模型与其它Web框架(或从缓存、队列等反序列化数据)结合起来包装ActionController::Parameters中的输入以使strong_parameters工作时,尽可能依赖Rails的开箱即用解析。

修复:目前还不清楚Rails应对这个问题的最佳方法是什么。Rails可以覆盖诸如JSON.parse之类的反序列化方法来返回ActionController::Parameters的实例,但这相对有侵入性,并且可能导致兼容性问题。

担心的开发者可以将strong_parameters与attr_accessible结合起来用于高度敏感的字段(如User#admin)以进行额外的保护,但这在大多数情况下可能会过犹不及。最后,这可能只是我们需要意识到和留心的行为。

感谢Brendon Murphy让我意识到这个问题。

感谢Adam Baldwin、Justin Collins、Neil Matatell、Noah Davis和Aaron Patterson对这篇文章的审阅。

使用HTML5创建电子书网站

在项目开始前,先要了解下HTML5规范包含的一些有用的新的语义标签,用于提供HTML页面的各个区域或部分的意义,例如页眉、页脚、导航栏、边栏等等。在以前的HTML版本中,这些部分通常使用<div>标签来创建,通过id或class属性来区分。

HTML5引入的主要标签包括:

标签 说明
header 此标签用于定义页面某些部分的页眉,可以是整个页面、article标签或section标签
nav 这是页面上主要导航链接的容器。此标签不应用于所有链接组,而是应仅用于主要导航块。如果你有一个footer标签包含导航链接,不需要将这些链接封装在nav标签中,因为footer标签将可以独自包括这些链接
footer 此标签定义页面的某些部分的页脚。页脚不一定是在页面、文章或区域的结尾,但是它通常是在那个位置
article 定义文档或页面上的独立区块,例如新闻、杂志、博文或评论
section 此标签表示文档的一部分,例如,文章或教程的一章或一节。该标签通常具有一个页眉,虽然严格来说是不需要的
aside 用于标记边栏或一些将认为与其周围内容有点无关的内容。此项的一个例子就是广告块
hgroup 在某些情况下,页面、文章或区域可能需要多个标题,例如,你有一个标题和一个副标题。你可以在hgroup标签中封装这些标题,使用h1标签表示主标题,h2标签表示副标题

这些标签的基础结构遵循以下大纲:

1
2
3
4
5
6
7
8
header
  hgroup
nav
article
  header
  section
    header
footer

上面是网站的布局设计,主要有header、navigation、footer和main四个区块,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!-- HTML5的DOCTYPE声明模式,它可以向后兼容,
     再也不用记忆XHTML中复杂的DOCTYPE了!-->
<!DOCTYPE html>
<html>
<head>
  <!-- 指定文档字符编码的写法,该写法在所有浏览器上都有效。-->
  <meta charset="utf-8" />
  <title>HTML5 Demo</title>
  <!-- link和script标签也无需提供type属性(No More Types for Scripts and Links),
       因为CSS和JavaScript是目前惟一受支持的样式表和脚本类型 -->
  <link href="style.css" rel="stylesheet" />
  <!-- 因为IE浏览器(甚至版本8)不支持新的HTML5标签,处理此问题的一个已知方法是使用
       JavaScript函数document.createElement()为每个标记创建虚拟标签。html5.js文件将
       为每个新的HTML标签进行此操作 -->
  <!--[if lt IE 9]>
    <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
  <![endif]-->
</head>

<body>
  <header>
    <hgroup>
      <h1>Logo</h1>
      <h2>Here is the slogan</h2>
    </hgroup>
  </header>

  <nav>
    <ul>
      <li><a href="#">Home</a></li>
      <li><a href="#">Business</a></li>
      <li><a href="#">History</a></li>
      <li><a href="#">Religion</a></li>
      <li><a href="#">Health</a></li>
      <li><a href="#">Science</a></li>
    </ul>
  </nav>

  <!-- main block begin -->
  <!-- main block end -->

  <footer>© 2013</footer>
</body>
</html>

对应的CSS文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
* {
  font-family: Lucida Sans, Arial, Helvetica, sans-serif;
}

body {
  width: 768px;
  margin: 0px auto;
}

header h1 {
  font-size: 36px;
  margin: 0px;
}

header h2 {
  font-size: 18px;
  margin: 0px;
  color: #888;
  font-style: italic;
}

nav ul {
  list-style: none;
  margin: 0;
  padding: 0;
  width: 100%;
}

nav ul:after {
  content: "\0020";
  display: block;
  height: 0;
  clear: both;
  visibility: hidden;
}

nav ul  li {
  float: left;
  width: 16.66%;
  text-align: center;
}

nav ul li a {
  display: block;
  background: #000;
  color: #fff;
  font-weight: bold;
  padding: 10px;
  border-right: 1px solid #fff;
  text-decoration: none;
}

nav ul li a:hover {
  background: #333;
}

footer {
  border-top: 1px solid #ccc;
  text-align: center;
  font-size: 12px;
  color: #888;
  margin-top: 24px;
}

整个网站主要有这么几个页面:首页、分类页、书目页、内容页,这些页面共用一个布局,主要区别在于main区块的不同。首页、分类页和书目页相似,都是由列表组成。

首页中有多个无序列表,列出网站中最新、最多被访问、最多被分享等的图书:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<h3>Latest</h3>
<ul>
  <li>Book's title</li>
  <li>Book's title</li>
  <li>Book's title</li>
</ul>

<h3>Most Viewed</h3>
<ul>
  <li>Book's title</li>
  <li>Book's title</li>
  <li>Book's title</li>
</ul>

<h3>Most Shared</h3>
<ul>
  <li>Book's title</li>
  <li>Book's title</li>
  <li>Book's title</li>
</ul>

分类页是单个无序列表,列出当前被访问分类下所有的图书:

1
2
3
4
5
6
<h3>Business</h3>
<ul>
  <li>Book's title</li>
  <li>Book's title</li>
  <li>Book's title</li>
</ul>

书目页是单个有序列表,列出某本书所有的章节:

1
2
3
4
5
6
<h3>Book's title</h3>
<ol>
  <li>Chapter 1</li>
  <li>Chapter 2</li>
  <li>Chapter 3</li>
</ol>

内容页,顾名思义就是显示具体内容的页面。文章内容用<article>标签表示,其中的标题、作者、发表时间等信息被包含在<header>标签中。主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<article>
  <header>
    <h1><a href="#">Chapter 1</a></h1>
    <p>By <a href="#">author</a> on <time>2013-05-24 14:54</time></p>
  </header>
  <p>
  Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed
  diam nonummy nibh euismod tincidunt ut laoreet dolore magna
  aliquam erat volutpat. Ut wisi enim ad minim veniam, quis
  nostrud exerci tation ullamcorper suscipit lobortis nisl ut
  aliquip ex ea commodo consequat.
  </p>
  <section>
    <header>
      <h1>Section heading</h1>
    </header>
    <p>
    Duis autem vel eum iriure dolor in hendrerit in vulputate velit
    esse molestie consequat, vel illum dolore eu feugiat nulla
    facilisis at vero eros et accumsan et iusto odio dignissim qui
    blandit praesent luptatum zzril delenit augue duis dolore te
    feugait nulla facilisi.
    </p>
  </section>
  <footer>
    <a href="#">Back</a>
    <a href="#">TOC</a>
    <a href="#">Forward</a>
  </footer>
</article>

在上面的代码里,<header>标签中我们仅使用了<h1>标签,这是因为HTML5会根据上下文计算出heading元素的层级,因此会有:

1
2
3
<body><h1>  <!-- 相当于heading 1 -->
<body><section><h1>  <!-- 相当于heading 2 -->
<body><section><section><h1>  <!-- 相当于heading 3 -->

最后顺便说一句,HTML5支持已存在的各种写法:xhtml1.0、xhtml1.1和html4.0,但建议使用xhtml1.1规范,即:

  1. 所有标签/属性都使用小写字母;
  2. 所有属性值都必须加引号;
  3. 使用闭合标签。

部署应用到Heroku时的问题

Heroku现在已经是纯粹的只读PaaS了,也就是说以前还支持的SQlite现在也不能使用了。因此部署到Heroku上的Rails应用需要把使用的数据库改成PostgreSQL,并且要关闭assets的预编译功能。

修改Gemfile,将SQLite换成PostgreSQL:

1
2
# gem 'sqlite3'
gem 'pg'

在config/application.rb中添加:

1
config.assets.initialize_on_precompile = false

股票功能需要导入交割单文件,因为导入后的文本文件不再使用,可以把上传路径由public/uploads改为tmp,这样就避免了Heroku不能写文件的问题。

应用上传后运行时出现异常,使用heroku logs -t查看日志发现有如下错误:

1
Error: column "stocks.share_name" must appear in the GROUP BY clause or be used in an aggregate function

这是因为在控制器中有这么一行代码:

1
current_user.stocks.select("share_code, share_name, sum(actual_amount) as amount").group("share_code")

在PostgreSQL中这会有问题。比如下面的数据表:

执行上面的SQL语句后,share_name的值到底是取Ruby呢还是ST Ruby?解决这个问题的方法是使用aggregate函数。

1
current_user.stocks.select("share_code, max(share_name) as share_name, sum(actual_amount) as amount").group("share_code")

使用PostgreSQL还有个问题,就是decimal类型的字段,取出来的值是字符串类型。例如:

1
if stock.amount < 0

它会报错误:

1
ArgumentError (comparison of String with 0 failed)

这个可以使用to_f函数解决:

1
if stock.amount.to_f < 0

敏捷开发走下坡路了吗?

英文原文:http://thatextramile.be/blog/2008/11/agile-development-going-downhill

James Shore(非常精彩的书《The Art Of Agile Development》的作者)在他的博客上写了一篇很有趣的帖子:The Decline And Fall Of Agile。你绝对应该读读它。

我想我同意James的观点。在过去的几年里,我已经听到很多的人说他们在做敏捷开发,实际上,他们几乎都不是。引用James的话:

But guess which part people adopt? That's right--Sprints and Scrums. Rapid cycles, but none of the good stuff that makes rapid cycles sustainable.

不幸的是,这是非常真实的。现在许多团队都在进行短迭代工作,很多团队也在做每日例会,或者Scrum,或者站立会议。但是,有多少人事实上致力于使敏捷开发成功的技术实践和原则呢?老实说,我一个也没见过。

我是真正的敏捷开发的铁杆迷,但即使是在我现在的工作中,我最近的两个团队也没有完全正确地实现它。我们的研究结果虽然还不错,但我认为我们仍然可以做得更好。我逐渐尝试引入更多的原则和实践,但它确实需要一些时间。但是,对敏捷开发的所有这些误解,许多人(开发者、项目经理、总经理等)都没有真正的帮助。在我当前的工作中,这些误解是非常小的,并没有真正的不良影响。以前在客户那里,我确实注意到这些误解是如何导致异常低效的情况。这实际上很可悲,因为管理者迟早可能会对敏捷方法持怀疑态度。如果这导致人们放弃一些技术实践和原则,对这个事业来说将会是一个很大的损失。

我觉得有更多的理由去读James的那本出色的书。如果我可以合法地迫使人们去阅读这本书,我会的:)

扫描ISBN条码实现藏书管理

每个程序猿家里都有一堆技术书籍,偶也不例外,因此想写个Android应用来管理自己的藏书以及想买的书籍。在网上找到marshal的识别图书ISBN号并输出查询结果的示例完善图书查询原型,增加收藏夹功能两篇文章,写的非常不错,还提供源代码。下载代码研究后发现已基本具备了想要的功能,决定在它的基础上做些修改供自己使用。

把原来uses-sdk的minSdkVersion改成了9,增加android:targetSdkVersion="17"。然后使用手机测试程序时发现,在连接网络时后台会抛出android.os.NetworkOnMainThreadException,并且应用崩溃打不开。通过查阅相关资料了解到,自从Android 2.3之后,系统增加了一个类StrictMode。这个类对网络的访问方式进行了一定的改变。官方文档给出了这个类设置的目的:

StrictMode is a developer tool which detects things you might be doing by accident and brings them to your attention so you can fix them.

StrictMode is most commonly used to catch accidental disk or network access on the application's main thread, where UI operations are received and animations take place. Keeping disk and network operations off the main thread makes for much smoother, more responsive applications. By keeping your application's main thread responsive, you also prevent ANR dialogs from being shown to users.

Note that even though an Android device's disk is often on flash memory, many devices run a filesystem on top of that memory with very limited concurrency. It's often the case that almost all disk accesses are fast, but may in individual cases be dramatically slower when certain I/O is happening in the background from other processes. If possible, it's best to assume that such things are not fast.

因为marshal把访问网络的代码直接放到UI线程中,造成和主线程的首要工作——UI交互——相矛盾。解决这类问题很容易,把访问网络的代码放到AsyncTask中就行了。

接着发现豆瓣API查询返回的是500错误,在浏览器上访问却又正常,后来给HttpClient加上Agent头就没问题了,不知道是不是期间豆瓣的API在实现上作了改变。

1
2
3
HttpClient client = new DefaultHttpClient();
String agent = System.getProperty("http.agent");
client.getParams().setParameter(CoreProtocolPNames.USER_AGENT, agent);

解析豆瓣XML查询结果的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
XmlPullParser parser = factory.newPullParser();
parser.setInput(inputStream, "utf-8");

Book book = new Book();
book.setIsbn(getIntent().getExtras().getString("ISBN"));

int eventType = parser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
    switch (eventType) {
    case XmlPullParser.START_TAG:
        if ("link".equals(parser.getName())
                && "image".equals(parser.getAttributeValue(null, "rel"))) {
            book.setImageUrl(parser.getAttributeValue(null, "href"));
            eventType = parser.next();
        } else if ("summary".equals(parser.getName())) {
            book.setSummary(parser.nextText());
        } else if ("attribute".equals(parser.getName())) {
            String name = parser.getAttributeValue(null, "name");
            if ("title".equals(name)) {
                book.setTitle(parser.nextText());
            } else if ("author".equals(name)) {
                book.setAuthor(parser.nextText());
            } else if ("publisher".equals(name)) {
                book.setPublisher(parser.nextText());
            }
        }
        break;
    case XmlPullParser.END_TAG:
        break;
    }
    eventType = parser.next();
}

然后,然后就是结果页面不显示图书信息。想要找到原因,肿么办?看来要调试WebView了!http://developer.android.com/guide/webapps/debugging.html 告诉了我们如何调试。

首先在WebView上设置setWebChromeClient方法:

1
2
3
4
5
6
7
webView.setWebChromeClient(new WebChromeClient() {
    public void onConsoleMessage(String message,
            int lineNumber, String sourceID) {
        Log.d(TAG, message + " -- From line "
                + lineNumber + " of " + sourceID);
    }
});

然后在JavaScript脚本中使用以下方法就可以在logcat中看到调试信息了。

1
2
3
4
console.log(String)
console.info(String)
console.warn(String)
console.error(String)

重新运行程序,果然在logcat中看到报如下错误:

1
Uncaught TypeError: Object [object Object] has no method 'xxx'

搜索后在Stack Overflow找到了问题的答案(Stack Overflow真的非常不错,问题的回答都非常详尽)。http://developer.android.com/guide/webapps/webview.html#BindingJavaScript 是官方文档的解释。

解决方法就是在要被JavaScript调用的方法上加@JavascriptInterface注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Book {

    @JavascriptInterface
    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        if (this.author != null) {
            this.author += ", " + author;
        } else {
            this.author = author;
        }
    }

代码下载:https://github.com/dohkoos/Bookly

绘制圆角的正确方法

英文原文:http://usabilitypost.com/2009/01/26/the-proper-way-to-draw-rounded-corners/

在网络上,我已经注意到当人们在他们的设计中实现圆角时犯了很多同样的错误。因为某些原因,当有另外一个圆角在它里面的时候,这个圆角会带来一个问题—无论是有一个围绕周围的边界,还是另一个圆角形状坐落在一个圆角形状内。

这里是我要说的:

我看到了很多这种类型的圆角。你可以看到圆角的半径和内圆角的是相同的。这是错误的,因为如果你保持相同半径,两个拐角之间的空间量不会自始至终相等。

下面是相等间距看起来的样子:

这是正确的做法。有什么不同?内角的半径被减小了。好吧,但你怎么知道半径应该有多大?这很简单—如果你把外面的圆角想象成一个圆形,你可以看到它的圆心在哪里。

把这个圆心同样地作为内角的圆心,你就会得到内角的半径。使用这种方法将确保两个形状之间的完美契合。

现在你知道了吧?这是做多个圆角彼此包含的正确办法。当然,不需要你使用精确的圆心—有时为了设计良好还需要你移动一点点。但请不要仅仅把相同的圆角向内移动—这是绝对错误的。

使用SiteMesh做网页布局

SiteMesh是一个基于GoF的Decorator模式,用于页面布局的框架。能帮助我们在由大量页面构成的项目中创建一致的页面布局和外观。

这里我们将会把它整合到JBookShelf里去。要和Struts 2整合,先在pom.xml添加以下插件,该插件会将依赖的SiteMesh也一并包含到项目中。

1
2
3
4
5
<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-sitemesh-plugin</artifactId>
    <version>2.3.12</version>
</dependency>

将web.xml配置中原来的

1
2
3
4
5
6
7
8
9
10
11
<filter>
    <filter-name>struts2</filter-name>
    <filter-class>
        org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter
    </filter-class>
</filter>

<filter-mapping>
    <filter-name>struts2</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

改成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<filter>
    <filter-name>struts-prepare</filter-name>
    <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareFilter</filter-class>
</filter>

<filter>
    <filter-name>sitemesh</filter-name>
    <filter-class>com.opensymphony.sitemesh.webapp.SiteMeshFilter</filter-class>
</filter>

<filter>
    <filter-name>struts-execute</filter-name>
    <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsExecuteFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>struts-prepare</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
    <filter-name>sitemesh</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
    <filter-name>struts-execute</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

这里要注意过滤器的位置。SiteMesh过滤器必须在StrutsPrepareFilter之后和StrutsExecuteFilter之前,否则在SiteMesh的修饰器页面中将访问不到ActionContext。这是因为Struts 2的所有值是保存在Stack Context或者ValueStack中的。默认情况下,某个过滤器一旦访问了该Stack Context或ValueStack,里面对应的值会被清洗掉。如果先使用Struts 2的StrutsPrepareAndExecuteFilter来过滤用户请求,则SiteMesh的过滤器将无法取得Stack Context或者ValueStack中的数据。为了解决这个问题,Struts 2提供了StrutsPrepareFilter和StrutsExecuteFilter类(在2.1.3版本前是ActionContextCleanUp和FilterDispatcher),通过它们协同来确保SiteMesh正常工作。

在WEB-INF下添加decorators.xml文档:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<decorators defaultdir="/layouts">
    <excludes>
        <pattern>/stylesheets/*</pattern>
        <pattern>/javascripts/*</pattern>
        <pattern>/images/*</pattern>
    </excludes>

    <decorator name="application" page="application.jsp">
        <pattern>/*</pattern>
    </decorator>
</decorators>

stylesheets、javascripts、images目录下的内容是不需要被修饰的,可以把它们放到execludes块中排除掉。

新建/layouts/application.jsp模版页:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<%@ taglib uri="/struts-tags" prefix="s" %>
<%@ taglib uri="http://www.opensymphony.com/sitemesh/decorator" prefix="decorator" %>

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title><decorator:title default="JBookShelf" /></title>
    <decorator:head />
</head>

<body>
    <div>
        <s:if test="#session.user_session_key != null">
        <s:a action="listBook">All Books</s:a>
        Welcome, you have logined.
        <s:a action="logout">Logout</s:a>
        </s:if>
        <s:else>
        <s:a action="login!input">Login</s:a> |
        <s:a action="register!input">Register</s:a>
        </s:else>
    </div>
    <hr />
    <div>Navigation</div>
    <hr />
    <decorator:body />
    <hr />
    <div>Footer</div>
</body>
</html>

代码下载:https://github.com/dohkoos/JBookShelf

使用ichartjs画2D条形图

在Rails中使用Open Flash Chart II中使用了OFC2来画2D条形图。现在Flash快要不行了,因为有了更好的HTML5。好的程序猿也要紧随潮流,使用新的技术来改进和增强他们的代码。这次就尝试使用HTML5图形库来替换OFC2。比较已有的一些图形库,最后选定国产的ichartjs

实现2D条形图真的很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<script type="text/javascript" src="ichart.1.1.min.js"></script>
<script type="text/javascript">
$(function() {
  var data = [
    {name: 'IE', value: 35.75, color: '#a5c2d5'},
    {name: 'Chrome', value: 29.84, color: '#cbab4f'},
    {name: 'Firefox', value: 24.88, color: '#76a871'},
    {name: 'Safari',value: 6.77, color: '#9f7961'},
    {name: 'Opera',value:2.02, color: '#a56f8f'},
    {name: 'Other',value: 0.73, color: '#6f83a5'}
  ];

  new iChart.Bar2D({
    render: 'canvasDiv',
    data: data,
    title: 'Top 5 Browsers from 1 to 29 Feb 2012',
    showpercent: true,
    decimalsnum: 2,
    width: 800,
    height: 400,
    coordinate: {
      scale: [{
        position: 'bottom',
        start_scale: 0,
        end_scale: 40,
        scale_space: 8,
        listeners: {
          parseText: function(t, x, y) {
            return {text: t + "%"}
          }
        }
      }]
    }
  }).draw();
});
</script>

<div id="canvasDiv"></div>