tg-firefly 0.9.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.
- data/.gitignore +11 -0
- data/.rspec +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +88 -0
- data/LICENSE +20 -0
- data/README.md +178 -0
- data/Rakefile +2 -0
- data/config.ru.example +43 -0
- data/firefly.gemspec +31 -0
- data/lib/firefly.rb +36 -0
- data/lib/firefly/base62.rb +25 -0
- data/lib/firefly/click.rb +14 -0
- data/lib/firefly/code_factory.rb +38 -0
- data/lib/firefly/config.rb +31 -0
- data/lib/firefly/server.rb +303 -0
- data/lib/firefly/share.rb +29 -0
- data/lib/firefly/url.rb +69 -0
- data/lib/firefly/version.rb +4 -0
- data/public/favicon.ico +0 -0
- data/public/images/facebook.png +0 -0
- data/public/images/hyves.png +0 -0
- data/public/images/qrcode.png +0 -0
- data/public/images/twitter.png +0 -0
- data/public/jquery-1.4.2.min.js +154 -0
- data/public/reset.css +48 -0
- data/public/style.css +98 -0
- data/spec/firefly/api_spec.rb +166 -0
- data/spec/firefly/base62_spec.rb +21 -0
- data/spec/firefly/code_factory_spec.rb +20 -0
- data/spec/firefly/server_spec.rb +74 -0
- data/spec/firefly/sharing_facebook_spec.rb +65 -0
- data/spec/firefly/sharing_hyves_spec.rb +107 -0
- data/spec/firefly/sharing_twitter_spec.rb +104 -0
- data/spec/firefly/url_spec.rb +124 -0
- data/spec/fixtures/urls.yml +11 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +54 -0
- data/views/error.haml +11 -0
- data/views/index.haml +80 -0
- data/views/info.haml +28 -0
- data/views/layout.haml +21 -0
- metadata +242 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module Firefly
|
3
|
+
class Base62
|
4
|
+
|
5
|
+
CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".split('')
|
6
|
+
BASE = 62
|
7
|
+
|
8
|
+
def self.encode(value)
|
9
|
+
s = ""
|
10
|
+
while value > 0
|
11
|
+
value, rem = value.divmod(BASE)
|
12
|
+
s << CHARS[rem]
|
13
|
+
end
|
14
|
+
s.reverse
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.decode(str)
|
18
|
+
total = 0
|
19
|
+
str.split('').reverse.each_with_index do |v,k|
|
20
|
+
total += (CHARS.index(v) * (BASE ** k))
|
21
|
+
end
|
22
|
+
total
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module Firefly
|
3
|
+
class Click
|
4
|
+
include DataMapper::Resource
|
5
|
+
|
6
|
+
# limit click abuse!
|
7
|
+
MAX_CLICKS_PER_IP = 10
|
8
|
+
|
9
|
+
property :id, Serial
|
10
|
+
property :ip, String, :index => true, :length => 20
|
11
|
+
property :code, String, :index => true, :length => 64
|
12
|
+
property :created_at, DateTime, :default => Proc.new{Time.now}
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module Firefly
|
3
|
+
class CodeFactory
|
4
|
+
class << self
|
5
|
+
attr_accessor :order
|
6
|
+
end
|
7
|
+
|
8
|
+
include DataMapper::Resource
|
9
|
+
|
10
|
+
property :id, Serial
|
11
|
+
property :count, Integer, :default => 0
|
12
|
+
|
13
|
+
# Redirect to the selected order method
|
14
|
+
def self.next_code!
|
15
|
+
self.send("next_#{self.order || :sequential}_code!".to_sym)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns the next value randomly
|
19
|
+
def self.next_random_code!
|
20
|
+
Firefly::Base62.encode(rand(200000000))
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the next auto increment value and updates
|
24
|
+
# the counter
|
25
|
+
def self.next_sequential_code!
|
26
|
+
code = nil
|
27
|
+
|
28
|
+
Firefly::CodeFactory.transaction do
|
29
|
+
c = Firefly::CodeFactory.first
|
30
|
+
code = Firefly::Base62.encode(c.count + 1)
|
31
|
+
c.update(:count => c.count + 1)
|
32
|
+
end
|
33
|
+
|
34
|
+
code
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module Firefly
|
3
|
+
class Config < Hash
|
4
|
+
|
5
|
+
DEFAULTS = {
|
6
|
+
:hostname => "localhost:3000",
|
7
|
+
:api_key => "test",
|
8
|
+
:database => "sqlite3://#{Dir.pwd}/firefly_#{ENV['RACK_ENV']}.sqlite3",
|
9
|
+
:recent_urls => 25,
|
10
|
+
:twitter => "Check this out: %short_url%",
|
11
|
+
:hyves_path => "toevoegen/tips",
|
12
|
+
:hyves_args => "type=12&rating=5",
|
13
|
+
:hyves_title => "Check this out",
|
14
|
+
:hyves_body => "Check this out: %short_url%",
|
15
|
+
:qr_size => "150x150"
|
16
|
+
}
|
17
|
+
|
18
|
+
def initialize obj
|
19
|
+
self.update DEFAULTS
|
20
|
+
self.update obj
|
21
|
+
end
|
22
|
+
|
23
|
+
def set key, val = nil, &blk
|
24
|
+
if val.is_a? Hash
|
25
|
+
self[key].update val
|
26
|
+
else
|
27
|
+
self[key] = block_given?? blk : val
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,303 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'sinatra/reloader'
|
4
|
+
require 'haml'
|
5
|
+
require 'digest/md5'
|
6
|
+
|
7
|
+
module Firefly
|
8
|
+
class InvalidUrlError < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
class InvalidCodeError < StandardError
|
12
|
+
end
|
13
|
+
|
14
|
+
class Server < Sinatra::Base
|
15
|
+
configure :development do
|
16
|
+
register Sinatra::Reloader
|
17
|
+
enable :logging, :dump_errors, :raise_errors
|
18
|
+
end
|
19
|
+
|
20
|
+
enable :sessions
|
21
|
+
|
22
|
+
dir = File.join(File.dirname(__FILE__), '..', '..')
|
23
|
+
|
24
|
+
set :root, dir
|
25
|
+
set :haml, {:format => :html5 }
|
26
|
+
set :session_secret, nil
|
27
|
+
|
28
|
+
attr_accessor :config
|
29
|
+
|
30
|
+
helpers do
|
31
|
+
include Rack::Utils
|
32
|
+
include Firefly::Share
|
33
|
+
alias_method :h, :escape_html
|
34
|
+
|
35
|
+
def url(*path_parts)
|
36
|
+
[ path_prefix, path_parts ].join("/").squeeze('/')
|
37
|
+
end
|
38
|
+
alias_method :u, :url
|
39
|
+
|
40
|
+
def path_prefix
|
41
|
+
request.env['SCRIPT_NAME']
|
42
|
+
end
|
43
|
+
|
44
|
+
def set_api_cookie(key)
|
45
|
+
session["firefly_session"] = Digest::MD5.hexdigest(key)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Taken from Rails
|
49
|
+
def truncate(text, length, options = {})
|
50
|
+
options[:omission] ||= "..."
|
51
|
+
|
52
|
+
length_with_room_for_omission = length - options[:omission].length
|
53
|
+
chars = text
|
54
|
+
stop = options[:separator] ?
|
55
|
+
(chars.rindex(options[:separator], length_with_room_for_omission) || length_with_room_for_omission) : length_with_room_for_omission
|
56
|
+
|
57
|
+
(chars.length > length ? chars[0...stop] + options[:omission] : text).to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
def has_valid_api_cookie?
|
61
|
+
key = session["firefly_session"]
|
62
|
+
key == Digest::MD5.hexdigest(config[:api_key])
|
63
|
+
end
|
64
|
+
|
65
|
+
def has_valid_share_key?
|
66
|
+
return false unless @config.has_key?(:sharing_key)
|
67
|
+
@config[:sharing_key] == params[:key]
|
68
|
+
end
|
69
|
+
|
70
|
+
def has_valid_share_domain?
|
71
|
+
return false unless @config.has_key?(:sharing_domains)
|
72
|
+
return true if @config[:sharing_domains].empty?
|
73
|
+
@config[:sharing_domains].any? { |domain| params[:url].include?(domain) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def has_valid_share_target?
|
77
|
+
return false unless @config.has_key?(:sharing_domains)
|
78
|
+
@config[:sharing_targets].include?(params[:target].downcase.to_sym)
|
79
|
+
end
|
80
|
+
|
81
|
+
def validate_share_permission
|
82
|
+
if has_valid_share_key? && has_valid_share_domain? && has_valid_share_target?
|
83
|
+
return true
|
84
|
+
else
|
85
|
+
status 401
|
86
|
+
return false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def validate_api_permission
|
91
|
+
true
|
92
|
+
# TODO: need api validation?
|
93
|
+
# if !has_valid_api_cookie? && params[:api_key] != config[:api_key]
|
94
|
+
# status 401
|
95
|
+
# return false
|
96
|
+
# else
|
97
|
+
# return true
|
98
|
+
# end
|
99
|
+
end
|
100
|
+
|
101
|
+
def short_url(url)
|
102
|
+
"http://#{config[:hostname]}/#{url.code}"
|
103
|
+
end
|
104
|
+
|
105
|
+
def generate_short_url(url = nil, user_id = nil, requested_code = nil)
|
106
|
+
code, result = nil, nil
|
107
|
+
|
108
|
+
begin
|
109
|
+
ff_url = Firefly::Url.shorten(url, user_id, requested_code)
|
110
|
+
code, result = ff_url.code, "http://#{config[:hostname]}/#{ff_url.code}"
|
111
|
+
rescue Firefly::InvalidUrlError
|
112
|
+
code, result = nil, "ERROR: The URL you posted is invalid."
|
113
|
+
rescue Firefly::InvalidCodeError
|
114
|
+
code, result = nil, "ERROR: The code is invalid or already exists."
|
115
|
+
rescue
|
116
|
+
code, result = nil, "ERROR: An unknown error occured"
|
117
|
+
end
|
118
|
+
|
119
|
+
return code, result
|
120
|
+
end
|
121
|
+
|
122
|
+
def is_highlighted?(url)
|
123
|
+
return false unless @highlight
|
124
|
+
@highlight.code == url.code
|
125
|
+
end
|
126
|
+
|
127
|
+
def store_api_key(key)
|
128
|
+
if key == config[:api_key]
|
129
|
+
set_api_cookie(config[:api_key])
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
before do
|
135
|
+
@authenticated = has_valid_api_cookie?
|
136
|
+
@config = config
|
137
|
+
@highlight = nil
|
138
|
+
@title = "Firefly at http://#{@config[:hostname]}"
|
139
|
+
|
140
|
+
set :session_secret, @config[:session_secret]
|
141
|
+
end
|
142
|
+
|
143
|
+
get '/' do
|
144
|
+
@highlight = Firefly::Url.first(:code => params[:highlight]) if not params[:highlight].nil?
|
145
|
+
@error = params[:highlight] == "error"
|
146
|
+
|
147
|
+
sort_column = params[:s] || 'created_at'
|
148
|
+
sort_order = params[:d] || 'desc'
|
149
|
+
|
150
|
+
@urls = Firefly::Url.all(:limit => config[:recent_urls], :order => [ sort_column.to_sym.send(sort_order.to_sym) ] )
|
151
|
+
|
152
|
+
haml :index
|
153
|
+
end
|
154
|
+
|
155
|
+
post '/api/set' do
|
156
|
+
store_api_key(params[:api_key])
|
157
|
+
redirect '/'
|
158
|
+
end
|
159
|
+
|
160
|
+
# GET /add?url=http://ariejan.net&api_key=test&user_id=42
|
161
|
+
# POST /add?url=http://ariejan.net&api_key=test&user_id=42
|
162
|
+
#
|
163
|
+
# Returns the shortened URL
|
164
|
+
api_add = lambda {
|
165
|
+
validate_api_permission or return "Permission denied: Invalid API key"
|
166
|
+
|
167
|
+
@url = params[:url]
|
168
|
+
@requested_code = params[:short]
|
169
|
+
@user_id = params[:user_id]
|
170
|
+
@code, @result = generate_short_url(@url, @user_id, @requested_code)
|
171
|
+
invalid = @code.nil?
|
172
|
+
|
173
|
+
if params[:visual]
|
174
|
+
store_api_key(params[:api_key])
|
175
|
+
@code.nil? ? haml(:error) : redirect("/?highlight=#{@code}")
|
176
|
+
else
|
177
|
+
head 422 if invalid
|
178
|
+
@result
|
179
|
+
end
|
180
|
+
}
|
181
|
+
|
182
|
+
get '/api/add', &api_add
|
183
|
+
post '/api/add', &api_add
|
184
|
+
|
185
|
+
api_share = lambda {
|
186
|
+
validate_share_permission or return "Cannot share that URL."
|
187
|
+
|
188
|
+
@url = params[:url]
|
189
|
+
@code, @result = generate_short_url(@url, nil)
|
190
|
+
invalid = @code.nil?
|
191
|
+
|
192
|
+
params[:title] ||= ""
|
193
|
+
title = URI.unescape(params[:title])
|
194
|
+
|
195
|
+
case (params[:target].downcase.to_sym)
|
196
|
+
when :twitter
|
197
|
+
redirect(twitter("http://#{config[:hostname]}/#{@code}", title))
|
198
|
+
when :hyves
|
199
|
+
redirect(hyves("http://#{config[:hostname]}/#{@code}", title))
|
200
|
+
when :facebook
|
201
|
+
redirect(facebook("http://#{config[:hostname]}/#{@code}"))
|
202
|
+
end
|
203
|
+
}
|
204
|
+
|
205
|
+
get '/api/share', &api_share
|
206
|
+
post '/api/share', &api_share
|
207
|
+
|
208
|
+
# GET /api/info/b3d
|
209
|
+
#
|
210
|
+
# Show info on the URL
|
211
|
+
get '/api/info/:code' do
|
212
|
+
validate_api_permission or return "Permission denied: Invalid API key"
|
213
|
+
|
214
|
+
@url = Firefly::Url.first(:code => params[:code])
|
215
|
+
|
216
|
+
if @url.nil?
|
217
|
+
status 404
|
218
|
+
"Sorry, that code is unknown."
|
219
|
+
else
|
220
|
+
@short_url = "http://#{config[:hostname]}/#{@url.code}"
|
221
|
+
haml :info
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# GET /b3d.png
|
226
|
+
#
|
227
|
+
# Return a QR code image
|
228
|
+
get '/:code.png' do
|
229
|
+
@url = Firefly::Url.first(:code => params[:code])
|
230
|
+
|
231
|
+
if @url.nil?
|
232
|
+
status 404
|
233
|
+
"Sorry, that code is unknown."
|
234
|
+
else
|
235
|
+
redirect("http://chart.googleapis.com/chart?cht=qr&chl=#{URI.escape(short_url(@url), Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))}&chs=#{config[:qr_size]}")
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# GET /b3d
|
240
|
+
#
|
241
|
+
# Redirect to the shortened URL
|
242
|
+
get '/:code' do
|
243
|
+
@url = Firefly::Url.first(:code => params[:code])
|
244
|
+
|
245
|
+
if @url.nil?
|
246
|
+
status 404
|
247
|
+
"Sorry, that code is unknown."
|
248
|
+
else
|
249
|
+
@url.register_click!(request.ip)
|
250
|
+
redirect @url.url, 301
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def initialize config = {}, &blk
|
255
|
+
super
|
256
|
+
@config = config.is_a?(Config) ? config : Firefly::Config.new(config)
|
257
|
+
@config.instance_eval(&blk) if block_given?
|
258
|
+
Firefly::CodeFactory.order = @config[:order]
|
259
|
+
begin
|
260
|
+
DataMapper.setup(:default, @config[:database])
|
261
|
+
DataMapper.auto_upgrade!
|
262
|
+
check_mysql_collation
|
263
|
+
check_code_factory
|
264
|
+
rescue
|
265
|
+
puts "Error setting up database connection. Please check the `database` setting in config.ru"
|
266
|
+
puts $!
|
267
|
+
puts "-------"
|
268
|
+
puts $!.backtrace
|
269
|
+
exit(1)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def check_code_factory
|
274
|
+
Firefly::CodeFactory.first || Firefly::CodeFactory.create(:count => 0)
|
275
|
+
end
|
276
|
+
|
277
|
+
def check_mysql_collation(first_try = true)
|
278
|
+
# Make sure the 'code' column is case-sensitive. This hack is for
|
279
|
+
# MySQL only, other database systems don't have this problem.
|
280
|
+
if DataMapper.repository(:default).adapter =~ "DataMapper::Adapters::MysqlAdapter"
|
281
|
+
query = "SHOW FULL COLUMNS FROM firefly_urls WHERE Field='code';"
|
282
|
+
collation = DataMapper.repository(:default).adapter.select(query)[0][:collation]
|
283
|
+
|
284
|
+
if collation != "utf8_bin"
|
285
|
+
if first_try
|
286
|
+
puts " ~ Your MySQL database is not using the 'utf8-bin' collation. Trying to fix..."
|
287
|
+
DataMapper.repository(:default).adapter.execute("ALTER TABLE firefly_urls MODIFY `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;")
|
288
|
+
return check_mysql_collation(false)
|
289
|
+
else
|
290
|
+
puts " ~ Failed to set the collation for `code` in `firefly_urls`. Please see http://wiki.github.com/ariejan/firefly/faq for details."
|
291
|
+
return false
|
292
|
+
end
|
293
|
+
else
|
294
|
+
if !first_try
|
295
|
+
puts " ~ Successfully fixed your database."
|
296
|
+
end
|
297
|
+
return true
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Firefly
|
2
|
+
module Share
|
3
|
+
def twitter(url, message = nil)
|
4
|
+
status = if message.nil? || message == ""
|
5
|
+
config[:twitter].gsub('%short_url%', url)
|
6
|
+
else
|
7
|
+
max_length = 140-1-url.size
|
8
|
+
[message.strip.slice(0...max_length), url].join(' ')
|
9
|
+
end
|
10
|
+
"http://twitter.com/home?status=#{URI.escape(status)}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def hyves(url, title = nil, body = nil)
|
14
|
+
if title.nil? || title == ""
|
15
|
+
title = config[:hyves_title]
|
16
|
+
end
|
17
|
+
|
18
|
+
if body.nil? || body == ""
|
19
|
+
body = config[:hyves_body].gsub('%short_url%', url)
|
20
|
+
end
|
21
|
+
|
22
|
+
"http://www.hyves.nl/profielbeheer/#{config[:hyves_path]}/?name=#{URI.escape(title.strip)}&text=#{URI.escape(body.strip)}&#{config[:hyves_args]}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def facebook(url)
|
26
|
+
"http://www.facebook.com/share.php?u=#{URI.escape(url)}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/firefly/url.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module Firefly
|
3
|
+
class Url
|
4
|
+
include DataMapper::Resource
|
5
|
+
|
6
|
+
VALID_CODE_REGEX = /^[A-Za-z0-9\-_]{3,64}$/u # case-insensitive, UTF-8 encoded
|
7
|
+
|
8
|
+
property :id, Serial
|
9
|
+
property :url, String, :index => true, :length => 255
|
10
|
+
property :code, String, :index => true, :length => 64
|
11
|
+
property :clicks, Integer, :default => 0
|
12
|
+
property :created_at, DateTime, :default => Proc.new{Time.now}
|
13
|
+
property :user_id, String, :index => true, :length => 50
|
14
|
+
# Increase the visits counter by 1
|
15
|
+
def register_click!(ip)
|
16
|
+
Firefly::Click.create(:code => code, :ip => ip)
|
17
|
+
if Firefly::Click.count(:code => code, :ip => ip) <= Firefly::Click::MAX_CLICKS_PER_IP
|
18
|
+
self.update(:clicks => self.clicks + 1)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Shorten a long_url and return a new FireFly::Url
|
23
|
+
def self.shorten(long_url, user_id, code = nil)
|
24
|
+
code = nil if code !~ /\S/
|
25
|
+
|
26
|
+
raise Firefly::InvalidUrlError.new unless valid_url?(long_url)
|
27
|
+
raise Firefly::InvalidCodeError.new unless valid_code?(code)
|
28
|
+
|
29
|
+
long_url = normalize_url(long_url)
|
30
|
+
|
31
|
+
the_url = Firefly::Url.first(:url => long_url, :user_id => user_id) || Firefly::Url.create(:url => long_url, :user_id => user_id)
|
32
|
+
return the_url unless the_url.code.nil?
|
33
|
+
|
34
|
+
code ||= get_me_a_code
|
35
|
+
the_url.update(:code => code)
|
36
|
+
the_url
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Generate a unique code, not already in use.
|
42
|
+
def self.get_me_a_code
|
43
|
+
code = Firefly::CodeFactory.next_code!
|
44
|
+
|
45
|
+
if Firefly::Url.count(:code => code) > 0
|
46
|
+
code = get_me_a_code
|
47
|
+
end
|
48
|
+
|
49
|
+
code
|
50
|
+
end
|
51
|
+
|
52
|
+
# Normalize the URL
|
53
|
+
def self.normalize_url(url)
|
54
|
+
url = URI.escape(URI.unescape(url))
|
55
|
+
URI.parse(url).normalize.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
# Validates the URL to be a valid http or https one.
|
59
|
+
def self.valid_url?(url)
|
60
|
+
url.match URI.regexp(['http', 'https'])
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.valid_code?(code)
|
64
|
+
return true if code.nil?
|
65
|
+
code.match(Firefly::Url::VALID_CODE_REGEX) && Firefly::Url.count(:code => code) == 0
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|