firefly 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,94 +3,100 @@ require 'haml'
3
3
  require 'digest/md5'
4
4
 
5
5
  module Firefly
6
+ class InvalidUrlError < StandardError
7
+ end
8
+
9
+ class InvalidCodeError < StandardError
10
+ end
11
+
6
12
  class Server < Sinatra::Base
7
13
  enable :sessions
8
-
14
+
9
15
  dir = File.join(File.dirname(__FILE__), '..', '..')
10
16
 
11
17
  set :views, "#{dir}/views"
12
18
  set :public, "#{dir}/public"
13
19
  set :haml, {:format => :html5 }
14
20
  set :static, true
15
-
21
+
16
22
  attr_accessor :config
17
-
23
+
18
24
  helpers do
19
25
  include Rack::Utils
20
26
  alias_method :h, :escape_html
21
-
27
+
22
28
  def url(*path_parts)
23
29
  [ path_prefix, path_parts ].join("/").squeeze('/')
24
30
  end
25
31
  alias_method :u, :url
26
-
32
+
27
33
  def path_prefix
28
34
  request.env['SCRIPT_NAME']
29
35
  end
30
-
36
+
31
37
  def set_api_cookie(key)
32
38
  session["firefly_session"] = Digest::MD5.hexdigest(key)
33
39
  end
34
-
40
+
35
41
  def has_valid_api_cookie?
36
42
  key = session["firefly_session"]
37
43
  key == Digest::MD5.hexdigest(config[:api_key])
38
44
  end
39
-
45
+
40
46
  def validate_api_permission
41
47
  if !has_valid_api_cookie? && params[:api_key] != config[:api_key]
42
48
  status 401
43
49
  return false
44
- else
45
- return true
50
+ else
51
+ return true
46
52
  end
47
53
  end
48
-
54
+
49
55
  def short_url(url)
50
56
  "http://#{config[:hostname]}/#{url.code}"
51
57
  end
52
-
58
+
53
59
  def generate_short_url(url = nil, requested_code = nil)
54
60
  code, result = nil, nil
55
61
 
56
- if !url.nil? && url != ""
62
+ begin
57
63
  ff_url = Firefly::Url.shorten(url, requested_code)
58
- if !ff_url.nil?
59
- code = ff_url.code
60
- result = "http://#{config[:hostname]}/#{code}"
61
- else
62
- code = nil
63
- result = "ERROR: The URL you posted is invalid."
64
- end
64
+ code, result = ff_url.code, "http://#{config[:hostname]}/#{ff_url.code}"
65
+ rescue Firefly::InvalidUrlError
66
+ code, result = nil, "ERROR: The URL you posted is invalid."
67
+ rescue Firefly::InvalidCodeError
68
+ code, result = nil, "ERROR: The code is invalid or already exists."
69
+ rescue
70
+ code, result = nil, "ERROR: An unknown error occured"
65
71
  end
66
-
72
+
67
73
  return code, result
68
74
  end
69
-
75
+
70
76
  def is_highlighted?(url)
71
77
  return false unless @highlight
72
78
  @highlight == url.code
73
79
  end
74
-
80
+
75
81
  # Format a tweet
76
82
  def tweet(url)
77
83
  config[:tweet].gsub('%short_url%', url)
78
84
  end
79
-
85
+
80
86
  def store_api_key(key)
81
87
  if key == config[:api_key]
82
88
  set_api_cookie(config[:api_key])
83
89
  end
84
90
  end
85
91
  end
86
-
92
+
87
93
  before do
88
94
  @authenticated = has_valid_api_cookie?
89
95
  @config = config
90
96
  @highlight = nil
91
- @title = "Firefly &mdash; #{@config[:hostname]}"
97
+ @title = "Firefly at http://#{@config[:hostname]}"
92
98
  end
93
-
99
+
94
100
  get '/' do
95
101
  @highlight = Firefly::Url.first(:code => params[:highlight])
96
102
  @error = params[:highlight] == "error"
@@ -102,12 +108,12 @@ module Firefly
102
108
 
103
109
  haml :index
104
110
  end
105
-
111
+
106
112
  post '/api/set' do
107
113
  store_api_key(params[:api_key])
108
114
  redirect '/'
109
115
  end
110
-
116
+
111
117
  # GET /add?url=http://ariejan.net&api_key=test
112
118
  # POST /add?url=http://ariejan.net&api_key=test
113
119
  #
@@ -117,23 +123,21 @@ module Firefly
117
123
 
118
124
  @url = params[:url]
119
125
  @requested_code = params[:short]
120
- @code, @result = generate_short_url(@url, @requested_code)
121
- invalid = @result =~ /you posted is invalid/
122
- @result ||= "Invalid URL specified."
123
-
126
+ @code, @result = generate_short_url(@url, @requested_code)
127
+ invalid = @code.nil?
128
+
124
129
  if params[:visual]
125
130
  store_api_key(params[:api_key])
126
- @code ||= "error"
127
- redirect "/?highlight=#{@code}"
131
+ @code.nil? ? haml(:error) : redirect("/?highlight=#{@code}")
128
132
  else
129
133
  head 422 if invalid
130
134
  @result
131
135
  end
132
136
  }
133
-
137
+
134
138
  get '/api/add', &api_add
135
139
  post '/api/add', &api_add
136
-
140
+
137
141
  # GET /b3d+
138
142
  #
139
143
  # Show info on the URL
@@ -141,7 +145,7 @@ module Firefly
141
145
  validate_api_permission or return "Permission denied: Invalid API key"
142
146
 
143
147
  @url = Firefly::Url.first(:code => params[:code])
144
-
148
+
145
149
  if @url.nil?
146
150
  status 404
147
151
  "Sorry, that code is unknown."
@@ -150,7 +154,7 @@ module Firefly
150
154
  haml :info
151
155
  end
152
156
  end
153
-
157
+
154
158
  # GET /api/export.csv
155
159
  #
156
160
  # Download a CSV file with all shortened URLs
@@ -217,7 +221,6 @@ module Firefly
217
221
  YAML::dump(output)
218
222
  end
219
223
 
220
-
221
224
  if defined? Barby
222
225
  # GET /b3d.png
223
226
  #
@@ -251,49 +254,54 @@ module Firefly
251
254
  redirect @url.url, 301
252
255
  end
253
256
  end
254
-
257
+
255
258
  def initialize config = {}, &blk
256
259
  super
257
260
  @config = config.is_a?(Config) ? config : Firefly::Config.new(config)
258
261
  @config.instance_eval(&blk) if block_given?
259
-
262
+
260
263
  begin
261
264
  DataMapper.setup(:default, @config[:database])
262
265
  DataMapper.auto_upgrade!
263
266
  check_mysql_collation
267
+ check_code_factory
264
268
  rescue
265
269
  puts "Error setting up database connection. Please check the `database` setting in config.ru"
266
- puts $!
267
- puts "-------"
268
- puts $!.backtrace
270
+ puts $!
271
+ puts "-------"
272
+ puts $!.backtrace
269
273
  exit(1)
270
274
  end
271
275
  end
272
276
 
273
- def check_mysql_collation(first_try = true)
274
- # Make sure the 'code' column is case-sensitive. This hack is for
275
- # MySQL only, other database systems don't have this problem.
276
- if DataMapper.repository(:default).adapter =~ "DataMapper::Adapters::MysqlAdapter"
277
- query = "SHOW FULL COLUMNS FROM firefly_urls WHERE Field='code';"
278
- collation = DataMapper.repository(:default).adapter.select(query)[0][:collation]
279
-
280
- if collation != "utf8_bin"
281
- if first_try
282
- puts " ~ Your MySQL database is not using the 'utf8-bin' collation. Trying to fix..."
283
- DataMapper.repository(:default).adapter.execute("ALTER TABLE firefly_urls MODIFY `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;")
284
- return check_mysql_collation(false)
285
- else
286
- puts " ~ Failed to set the collation for `code` in `firefly_urls`. Please see http://wiki.github.com/ariejan/firefly/faq for details."
287
- return false
288
- end
289
- else
290
- if !first_try
291
- puts " ~ Successfully fixed your database."
292
- end
293
- return true
294
- end
295
- end
296
- end
277
+ def check_code_factory
278
+ Firefly::CodeFactory.first || Firefly::CodeFactory.create(:count => 0)
279
+ end
280
+
281
+ def check_mysql_collation(first_try = true)
282
+ # Make sure the 'code' column is case-sensitive. This hack is for
283
+ # MySQL only, other database systems don't have this problem.
284
+ if DataMapper.repository(:default).adapter =~ "DataMapper::Adapters::MysqlAdapter"
285
+ query = "SHOW FULL COLUMNS FROM firefly_urls WHERE Field='code';"
286
+ collation = DataMapper.repository(:default).adapter.select(query)[0][:collation]
287
+
288
+ if collation != "utf8_bin"
289
+ if first_try
290
+ puts " ~ Your MySQL database is not using the 'utf8-bin' collation. Trying to fix..."
291
+ DataMapper.repository(:default).adapter.execute("ALTER TABLE firefly_urls MODIFY `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;")
292
+ return check_mysql_collation(false)
293
+ else
294
+ puts " ~ Failed to set the collation for `code` in `firefly_urls`. Please see http://wiki.github.com/ariejan/firefly/faq for details."
295
+ return false
296
+ end
297
+ else
298
+ if !first_try
299
+ puts " ~ Successfully fixed your database."
300
+ end
301
+ return true
302
+ end
303
+ end
304
+ end
297
305
  end
298
306
  end
299
307
 
data/lib/firefly/url.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  module Firefly
2
2
  class Url
3
3
  include DataMapper::Resource
4
-
4
+
5
5
  VALID_URL_REGEX = /^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/ix
6
6
  VALID_CODE_REGEX = /^[a-z0-9\-_]{3,64}$/i
7
7
 
@@ -10,33 +10,47 @@ module Firefly
10
10
  property :code, String, :index => true, :length => 64
11
11
  property :clicks, Integer, :default => 0
12
12
  property :created_at, DateTime, :default => Proc.new{Time.now}
13
-
13
+
14
14
  # Increase the visits counter by 1
15
15
  def register_click!
16
16
  self.update(:clicks => self.clicks + 1)
17
17
  end
18
-
18
+
19
19
  # Shorten a long_url and return a new FireFly::Url
20
20
  def self.shorten(long_url, code = nil)
21
21
  code = nil if code !~ /\S/
22
22
 
23
- return nil unless valid_url?(long_url)
24
- return nil unless valid_code?(code)
23
+ raise Firefly::InvalidUrlError.new unless valid_url?(long_url)
24
+ raise Firefly::InvalidCodeError.new unless valid_code?(code)
25
25
 
26
26
  long_url = normalize_url(long_url)
27
-
27
+
28
28
  the_url = Firefly::Url.first(:url => long_url) || Firefly::Url.create(:url => long_url)
29
- code ||= Firefly::Base62.encode(the_url.id.to_i)
30
- the_url.update(:code => code) if the_url.code.nil?
29
+ return the_url unless the_url.code.nil?
30
+
31
+ code ||= get_me_a_code
32
+ the_url.update(:code => code)
31
33
  the_url
32
- end
33
-
34
+ end
35
+
34
36
  private
37
+
38
+ # Generate a unique code, not already in use.
39
+ def self.get_me_a_code
40
+ code = Firefly::CodeFactory.next_code!
41
+
42
+ if Firefly::Url.count(:code => code) > 0
43
+ code = get_me_a_code
44
+ end
45
+
46
+ code
47
+ end
48
+
35
49
  # Normalize the URL
36
50
  def self.normalize_url(url)
37
51
  URI.parse(URI.escape(url)).normalize.to_s
38
52
  end
39
-
53
+
40
54
  # Validates the URL to be a valid http or https one.
41
55
  def self.valid_url?(url)
42
56
  url.match(Firefly::Url::VALID_URL_REGEX)
@@ -1,3 +1,3 @@
1
1
  module Firefly
2
- Version = File.read(File.join(File.dirname(__FILE__), '..', '..', 'VERSION')).strip
3
- end
2
+ VERSION = "1.3.0"
3
+ end
data/public/style.css CHANGED
@@ -2,7 +2,7 @@ html { background:#efefef; font-family:Arial, Verdana, sans-serif; font-size:13p
2
2
  body { padding:0; margin:0; }
3
3
 
4
4
  .header { background:#000; padding:8px 5% 0 5%; border-bottom:1px solid #444;border-bottom:5px solid #ce1212;}
5
- .header h1 { color:#333; font-size:90%; font-weight:bold; margin-bottom:6px;}
5
+ .header h1 { color:#e0e0e0; font-size:140%; font-weight:bold; margin-bottom:12px; margin-top:6px;}
6
6
  .header ul li { display:inline;}
7
7
  .header ul li a { color:#fff; text-decoration:none; margin-right:10px; display:inline-block; padding:8px; -webkit-border-top-right-radius:6px; -webkit-border-top-left-radius:6px; -moz-border-radius-topleft:6px; -moz-border-radius-topright:6px; }
8
8
  .header ul li a:hover { background:#333;}
@@ -58,7 +58,7 @@ body { padding:0; margin:0; }
58
58
  #footer { padding:10px 5%; background:#efefef; color:#999; font-size:85%; line-height:1.5; border-top:5px solid #ccc; padding-top:10px;}
59
59
  #footer p a { color:#999;}
60
60
 
61
- #main p.poll { background:url(poll.png) no-repeat 0 2px; padding:3px 0; padding-left:23px; float:right; font-size:85%; }
61
+ #main blockquote { background: #efefef; color: #999; line-height: 1.5; margin: 10px; padding: 10px; }
62
62
 
63
63
  #main ul.failed {}
64
64
  #main ul.failed li {background:-webkit-gradient(linear, left top, left bottom, from(#efefef), to(#fff)) #efefef; margin-top:10px; padding:10px; overflow:hidden; -webkit-border-radius:5px; border:1px solid #ccc; }
@@ -90,5 +90,5 @@ body { padding:0; margin:0; }
90
90
  #main table tr td.label a { text-decoration: none; font-size: 11px; }
91
91
  #main table tr td.label a.highlight { color: #f00; }
92
92
 
93
- #main table tr td input.short_url { border: 1px solid #CCC; color: #666; font-family: Monaco, 'Courier New', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 11px; height: 16px; padding: 3px 5px 2px; width: 160px; }
93
+ #main table tr td input.short_url { border: 1px solid #CCC; color: #666; font-family: Monaco, 'Courier New', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 11px; height: 16px; padding: 3px 5px 2px; width: 200px; }
94
94
  #main table tr td img.twitter { border: 0; margin: 0; padding: 0; float: right; width: 16px; height: 16px; }
@@ -6,7 +6,7 @@ describe "API" do
6
6
  def app
7
7
  @@app
8
8
  end
9
-
9
+
10
10
  [:post, :get].each do |verb|
11
11
  describe "adding a URL by #{verb.to_s.upcase}" do
12
12
  it "should be okay adding a new URL" do
@@ -31,58 +31,58 @@ describe "API" do
31
31
 
32
32
  it "should permit including a requested short code" do
33
33
  self.send verb, '/api/add', :url => "http://example.org", :short => 'orz', :api_key => 'test'
34
-
34
+
35
35
  last_response.should be_ok
36
36
  last_response.body.should eql("http://test.host/orz")
37
37
  end
38
38
 
39
- it "should not permit too-short, too-long, or duplicate short codes" do
39
+ it "should not allow the same short code twice" do
40
40
  self.send verb, '/api/add', :url => "http://example.org", :short => 'orz', :api_key => 'test'
41
41
  last_response.should be_ok
42
42
  self.send verb, '/api/add', :url => "http://example.com", :short => 'orz', :api_key => 'test'
43
43
  last_response.should_not be_ok
44
- last_response.body.should match("The URL you posted is invalid")
44
+ end
45
+
46
+ it "should not allow short codes of size < 3" do
45
47
  self.send verb, '/api/add', :url => "http://example.org", :short => 'or', :api_key => 'test'
46
48
  last_response.should_not be_ok
47
- last_response.body.should match("The URL you posted is invalid")
48
- self.send verb, '/api/add', :url => "http://example.org", :short => 'orz' * 37, :api_key => 'test'
49
+ end
50
+
51
+ it "should not allow short codes of size > 64" do
52
+ self.send verb, '/api/add', :url => "http://example.org", :short => 'x' * 65, :api_key => 'test'
49
53
  last_response.should_not be_ok
50
- last_response.body.should match("The URL you posted is invalid")
51
54
  end
52
55
 
53
56
  it "should show an error when shortening an invalid URL" do
54
57
  self.send verb, '/api/add', :url => 'ftp://example.org', :api_key => 'test'
55
-
56
58
  last_response.body.should match("The URL you posted is invalid")
57
59
  end
58
-
60
+
59
61
  it "should show an error when shortening an invalid URL in visual mode" do
60
62
  self.send verb, '/api/add', :url => 'ftp://example.org', :api_key => 'test', :visual => "1"
61
- follow_redirect!
62
-
63
63
  last_response.body.should match("The URL you posted is invalid")
64
64
  end
65
-
65
+
66
66
  it "should redirect to the highlighted URL when visual is enabled" do
67
67
  self.send verb, '/api/add', :url => 'http://example.org/', :api_key => 'test', :visual => "1"
68
68
  follow_redirect!
69
-
69
+
70
70
  last_request.path.should eql("/")
71
71
  last_request.should be_get
72
72
  end
73
-
73
+
74
74
  it "should store the API key in the session with visual enabled" do
75
75
  self.send verb, '/api/add', :url => 'http://example.org/', :api_key => 'test', :visual => "1"
76
- follow_redirect!
77
-
76
+ follow_redirect!
77
+
78
78
  last_response.body.should_not match(/API Key/)
79
79
  end
80
-
80
+
81
81
  it "should highlight the shortened URL" do
82
82
  self.send verb, '/api/add', :url => 'http://example.org/', :api_key => 'test', :visual => "1"
83
83
  url = Firefly::Url.first(:url => "http://example.org/")
84
- follow_redirect!
85
-
84
+ follow_redirect!
85
+
86
86
  last_request.query_string.should match(/highlight=#{url.code}/)
87
87
  end
88
88
 
@@ -91,10 +91,10 @@ describe "API" do
91
91
  last_response.status.should eql(401)
92
92
  end
93
93
 
94
- it "should not return a shortened URL on 401" do
94
+ it "should not return a shortened URL on 401" do
95
95
  self.send verb, '/api/add', :url => 'http://example.org', :api_key => 'false'
96
- last_response.body.should match(/Permission denied: Invalid API key/)
97
- end
96
+ last_response.body.should match(/Permission denied: Invalid API key/)
97
+ end
98
98
 
99
99
  it "should create a new Firefly::Url" do
100
100
  lambda {
@@ -107,42 +107,42 @@ describe "API" do
107
107
 
108
108
  lambda {
109
109
  self.send verb, '/api/add', :url => 'http://example.org', :api_key => 'test'
110
- }.should_not change(Firefly::Url, :count).by(1)
110
+ }.should_not change(Firefly::Url, :count).by(1)
111
111
  end
112
112
  end
113
- end
114
-
113
+ end
114
+
115
115
  describe "getting information" do
116
116
  before(:each) do
117
117
  @created_at = Time.now
118
118
  @url = Firefly::Url.create(:url => 'http://example.com/123', :code => 'alpha', :clicks => 69, :created_at => @created_at)
119
119
  end
120
-
120
+
121
121
  it "should work" do
122
122
  get '/api/info/alpha', :api_key => "test"
123
123
  last_response.should be_ok
124
124
  end
125
-
125
+
126
126
  it "should show the click count" do
127
127
  get '/api/info/alpha', :api_key => "test"
128
128
  last_response.body.should match(/69/)
129
129
  end
130
-
130
+
131
131
  it "should show the short URL" do
132
132
  get '/api/info/alpha', :api_key => "test"
133
133
  last_response.body.should match(/alpha/)
134
134
  end
135
-
135
+
136
136
  it "should show the shortened at time" do
137
137
  get '/api/info/alpha', :api_key => "test"
138
138
  last_response.body.should match(/#{@created_at.strftime("%Y-%m-%d %H:%M")}/)
139
139
  end
140
-
140
+
141
141
  it "should show the full URL" do
142
142
  get '/api/info/alpha', :api_key => "test"
143
143
  last_response.body.should match(/http:\/\/example.com\/123/)
144
144
  end
145
-
145
+
146
146
  it "should validate API permissions" do
147
147
  get '/api/info/alpha', :api_key => false
148
148
  last_response.status.should be(401)
@@ -175,10 +175,10 @@ describe "API" do
175
175
  Firefly::Server.new do
176
176
  set :hostname, "test.host"
177
177
  set :api_key, "test#!"
178
- set :database, "sqlite3://#{Dir.pwd}/firefly_test.sqlite3"
178
+ set :database, "mysql://root@localhost/firefly_test"
179
179
  end
180
180
  end
181
-
181
+
182
182
  it "should be okay adding a new URL" do
183
183
  self.send :get, '/api/add', :url => 'http://example.org/api_key_test', :api_key => 'test#!'
184
184
  last_response.should be_ok