fir 0.0.9 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,4 @@
1
1
  ---
2
2
  johndoe:
3
3
  salt: b64e9a6e7e2c187a81a30de2d0
4
- # Password is "foo"
5
4
  crypted_password: f8463d3c8d229f4daec934d23db3b6f1d5b5eedc56a965c6721ed8b697e60a7d4dfe7a9e249eb3fc02d00c9528852b20bda276ff6ec05e972e7f5f338b1aa59e
@@ -53,12 +53,14 @@ module Fir
53
53
  if match
54
54
  page = Fir.sanitize_page(match[1])
55
55
  page_path = File.join(FIR_ROOT, 'pages', page)
56
- if File.exists?(page_path)
56
+ if env['REQUEST_METHOD'].downcase == 'post'
57
+ update_page(page_path, env)
58
+ elsif File.exists?(page_path)
57
59
  case env['REQUEST_METHOD'].downcase
58
60
  when 'get'
59
61
  retrieve_page(page_path)
60
- when 'post'
61
- update_page(page_path, env)
62
+ when 'delete'
63
+ return method_not_allowed('DELETE not yet implemented')
62
64
  else
63
65
  return method_not_allowed('Only GET and POST are allowed')
64
66
  end
@@ -109,7 +111,7 @@ module Fir
109
111
  if File.readable?(page_path)
110
112
  source = File.read(page_path)
111
113
  directory, filename, ext = Fir.split_path(page_path)
112
- filename_with_ext = filename + '.' + ext
114
+ filename_with_ext = filename + ext
113
115
  [200, {'Content-Type' => 'text/plain', 'Content-Disposition' => "attachment; #{filename_with_ext}"}, source]
114
116
  else
115
117
  internal_server_error('File exists on disk, but is not readable. Are the permissions wrong?')
@@ -117,12 +119,13 @@ module Fir
117
119
  end
118
120
 
119
121
  def update_page(page_path, env)
120
- if File.writable?(page_path)
122
+ if File.writable?(page_path) or (!File.exists?(page_path) and File.writable?(File.dirname(page_path)))
121
123
  File.open(page_path, 'w') do |file|
122
124
  request = ::Rack::Request.new(env)
123
125
  file.write(request.POST['content'])
124
126
  end
125
- Fir.clear_cache(page_path)
127
+ # page_path will be an absolute path at this point, but clear_cache expects it to be relative to the pages directory
128
+ Fir.clear_cache(page_path.sub(File.join(FIR_ROOT, 'pages'), ''))
126
129
  [200, {'Content-Type' => 'text/plain'}, "#{page_path} has been saved."]
127
130
  else
128
131
  internal_server_error('File exists on disk, but is not writeable. Are the permissions wrong?')
@@ -0,0 +1,33 @@
1
+ module Fir
2
+ def self.generate!(args)
3
+ ::Fir::Generator.new.generate!(args)
4
+ end
5
+
6
+ class Generator
7
+ def generate!(args)
8
+ unless args.length >= 1
9
+ raise 'Usage: fir path'
10
+ end
11
+
12
+ fir_root = args.shift
13
+
14
+ if File.exists?(fir_root)
15
+ raise "#{fir_root} already exists! Aborting."
16
+ end
17
+ FileUtils.cp_r FIR_SKELETON_ROOT, fir_root
18
+ # What's the deal with .htaccess being renamed, you ask? Rubygems doesn't want to include .htaccess
19
+ # in the package, so we have to distribute it as htaccess and then rename it at the last moment.
20
+ FileUtils.mv File.join(fir_root, 'public', 'htaccess'), File.join(fir_root, 'public', '.htaccess')
21
+ puts "Created new Fir site in #{fir_root}"
22
+
23
+ [
24
+ ['--with-dispatch-cgi', 'public/dispatch.cgi'],
25
+ ['--with-htaccess', 'public/.htaccess']
26
+ ].each do |option, file|
27
+ unless args.include?(option)
28
+ FileUtils.rm File.join(fir_root, file)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -8,7 +8,8 @@ module Fir
8
8
  require 'fir'
9
9
  require 'active_support/secure_random'
10
10
  unless (ENV.has_key?('user') or ENV.has_key?('USER')) and (ENV.has_key?('pass') or ENV.has_key?('PASS'))
11
- raise 'usage: rake users:add user=username pass=password'
11
+ puts 'usage: rake users:add user=username pass=password'
12
+ exit
12
13
  end
13
14
  user = ENV['user'] || ENV['USER']
14
15
  pass = ENV['pass'] || ENV['PASS']
@@ -27,13 +28,20 @@ module Fir
27
28
  end
28
29
  end
29
30
  end
30
- desc 'Export the site to static HTML. (For web hosts that can\'t support Rack. But mod_rewrite must be enabled for .htaccess!) ' +
31
+ desc 'Export the site to static HTML. (For web hosts that don\'t support Rack. Creates an .htaccess file for pretty URLs. mod_rewrite must be enabled!) ' +
31
32
  'Usage: rake export dir=path_to_export_to'
32
33
  task :export do
33
34
  require 'fileutils'
34
35
 
35
36
  if !ENV.has_key?('dir') or ENV['dir'].empty?
36
- raise(ArgumentError, 'Usage: rake export dir=path_to_export_to')
37
+ puts 'Usage: rake export dir=path_to_export_to'
38
+ exit
39
+ end
40
+
41
+ copy_to = ENV['dir']
42
+ unless ENV['force'] or !File.exists?(copy_to) or Dir.entries(copy_to).length == 2
43
+ puts 'Directory is not empty. Export aborted. Use "rake export dir=path force=1" to override. This will delete everything in the directory!'
44
+ exit
37
45
  end
38
46
 
39
47
  page_config = YAML.load(File.read(File.join(FIR_ROOT, 'pages.yml')))
@@ -54,7 +62,7 @@ module Fir
54
62
  result
55
63
  end + path_mappings.keys
56
64
 
57
- puts 'Iniitalizing Fir site...'
65
+ puts "Iniitalizing Fir site in #{copy_to}..."
58
66
 
59
67
  Fir.config.force_caching = true
60
68
  require File.expand_path(File.join(FIR_ROOT, 'config.rb'))
@@ -67,10 +75,10 @@ module Fir
67
75
  end
68
76
 
69
77
  copy_from = File.join(FIR_ROOT, 'public', '.')
70
- copy_to = ENV['dir']
71
- unless File.exists?(copy_to)
72
- Dir.mkdir(copy_to)
78
+ if File.exists?(copy_to)
79
+ FileUtils.rm_rf(copy_to)
73
80
  end
81
+ Dir.mkdir(copy_to)
74
82
  FileUtils.cp_r(copy_from, copy_to)
75
83
  FileUtils.cp_r(File.join(copy_to, 'cache', '.'), copy_to)
76
84
  FileUtils.rm_rf(File.join(copy_to, 'cache'))
@@ -91,6 +99,49 @@ module Fir
91
99
  f.write(htaccess)
92
100
  end
93
101
  end
102
+
103
+ desc "Export the site to static HTML and transfer it to the server using rsync and SSH. (For web hosts that don't support Rack. " +
104
+ "Creates an .htaccess file for pretty URLs. mod_rewrite must be enabled!) " +
105
+ "Example usage: rake export_to_server dest=username@example.com:/home/username/public_html\n\n" +
106
+ "Obviously, dest should be set to the correct rsync destination for your deploy environment. " +
107
+ "Note that dest is NOT a URI, even though it sort of looks like one. It's a destination string in the form that rsync accepts."
108
+ task :export_to_server do
109
+ begin
110
+ ENV['dir'] = '.server_export_tmp'
111
+ Rake::Task['export'].execute
112
+ if ENV['dest']
113
+ dest = ENV['dest']
114
+ elsif File.exists?(File.join(FIR_ROOT, 'deploy.yml'))
115
+ yaml = YAML.load(File.read(File.join(FIR_ROOT, 'deploy.yml')))
116
+ ['host', 'user', 'path'].each do |param|
117
+ unless yaml.has_key?(param)
118
+ puts "deploy.yml must specify #{param}"
119
+ exit
120
+ end
121
+ end
122
+ dest = "#{yaml['user']}@#{yaml['host']}:#{yaml['path']}"
123
+ else
124
+ puts "Example usage: rake export_to_server dest=username@example.com/home:/username/public_html"
125
+ exit
126
+ end
127
+ Rsyncer.new.run(dest)
128
+ ensure
129
+ FileUtils.rm_rf '.server_export_tmp'
130
+ end
131
+ end
132
+ end
133
+
134
+ class Rsyncer
135
+ def run(dest)
136
+ run_command("rsync -ave ssh .server_export_tmp/* #{dest}")
137
+ end
138
+
139
+ private
140
+
141
+ # Stupid indirection to make it testable with should_receive and/or stub. (We don't want to run the real rsync from our test suite.)
142
+ def run_command(command)
143
+ system command
144
+ end
94
145
  end
95
146
  end
96
147
  end
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/ruby
2
+
3
+ # This script is called when running Fir as a CGI, which isn't recommended.
4
+ # Still, here it is, in case you want to try.
5
+ #
6
+ # This script will not be operational unless the webserver is configured properly.
7
+ # In Apache, you need to enable mod_rewrite in .htaccess files for this directory.
8
+ # That usually means setting AllowOverride to All (http://httpd.apache.org/docs/1.3/mod/core.html#allowoverride).
9
+ # Apache must also be told that CGI is allowed in this directory. This is done with the ExecCGI option
10
+ # (http://httpd.apache.org/docs/1.3/mod/core.html#options).
11
+ #
12
+ # Learn more about CGI under Apache: http://httpd.apache.org/docs/1.3/howto/cgi.html
13
+ #
14
+ # You must also set up rewrite rules so the requests get sent to this script.
15
+ # See how it's done in .htaccess in this directory.
16
+ #
17
+ # Besides the performance issues, CGI has another disadvantage. Apache (and perhaps others) unescapes
18
+ # URLs before sending them to your CGI script. Passenger does not. Fir, being designed for Passenger,
19
+ # unescapes URLs. So, when running Fir as a CGI, all URLs will be escaped twice. This breaks certain URLs.
20
+ # For example:
21
+ #
22
+ # http://example.com/the%2Bsign
23
+ #
24
+ # ...will break. Apache will unescape the path to "the+sign." Fir will then unescape it again to "the sign."
25
+
26
+ require 'rubygems'
27
+ require 'fir'
28
+
29
+ FIR_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
30
+ require File.expand_path(File.join(FIR_ROOT, 'config.rb'))
31
+
32
+ builder = Rack::Builder.new(&Fir.boot_proc)
33
+
34
+ Rack::Handler::CGI.run builder
@@ -0,0 +1,3 @@
1
+ RewriteEngine On
2
+ RewriteCond %{REQUEST_FILENAME} !-f
3
+ RewriteRule ^(.*)$ dispatch.cgi/$1 [QSA,L]
@@ -0,0 +1,172 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe 'admin interface' do
4
+ before :each do
5
+ load_config
6
+ end
7
+
8
+ it 'returns 404 for bad URLs' do
9
+ authorize 'johndoe', 'foo'
10
+ get '/-admin/doesnt_exist'
11
+ last_response.should be_not_found
12
+ end
13
+
14
+ describe 'GET pages' do
15
+ it 'requires HTTP auth' do
16
+ get '/-admin/pages'
17
+ last_response.status.should == 401
18
+ end
19
+
20
+ it 'does not accept bad user/pass combos' do
21
+ authorize 'johndoe', 'wrong'
22
+ get '/-admin/pages'
23
+ last_response.status.should == 401
24
+ end
25
+
26
+ it 'returns an XML feed with each directory and page' do
27
+ require 'test/unit/xml'
28
+ include Test::Unit::XML
29
+ authorize 'johndoe', 'foo'
30
+ get '/-admin/pages'
31
+ expected =
32
+ '<?xml version="1.0" encoding="UTF-8"?>
33
+ <pages>
34
+ <page>
35
+ <name>404.html.erb</name>
36
+ </page>
37
+ <page>
38
+ <name>contact-us.markdown</name>
39
+ </page>
40
+ <page>
41
+
42
+ <name>index.markdown</name>
43
+ </page>
44
+ <folder>
45
+ <name>products</name>
46
+ <pages>
47
+ <page>
48
+ <name>dynamite-plus.html.erb</name>
49
+
50
+ </page>
51
+ <page>
52
+ <name>dynamite.html.erb</name>
53
+ </page>
54
+ <page>
55
+ <name>index.markdown</name>
56
+ </page>
57
+ <page>
58
+
59
+ <name>mouse-trap.html.erb</name>
60
+ </page>
61
+ </pages>
62
+ </folder>
63
+ </pages>'.gsub(/\s/, '')
64
+
65
+ last_response.body.gsub(/\s/, '').should == expected
66
+ end
67
+ end
68
+
69
+ describe 'GET page/pagename' do
70
+ it 'requires HTTP auth' do
71
+ get '/-admin/pages/index.markdown'
72
+ last_response.status.should == 401
73
+ end
74
+
75
+ it 'does not accept bad user/pass combos' do
76
+ authorize 'johndoe', 'wrong'
77
+ get '/-admin/pages/index.markdown'
78
+ last_response.status.should == 401
79
+ end
80
+
81
+ context 'with balid login' do
82
+ before :each do
83
+ authorize 'johndoe', 'foo'
84
+ get '/-admin/pages/index.markdown'
85
+ end
86
+
87
+ it 'returns the source code of the page' do
88
+ last_response.body.should == File.read(File.join(FIR_ROOT, 'pages', 'index.markdown'))
89
+ end
90
+
91
+ it 'sets Content-Type to text/plain' do
92
+ last_response.content_type.should == 'text/plain'
93
+ end
94
+
95
+ it 'sets Content-Disposition to attachment with the source filename' do
96
+ last_response.headers['Content-Disposition'].should == 'attachment; index.markdown'
97
+ end
98
+
99
+ it 'works for pages in sub-directories' do
100
+ get '/-admin/pages/products/index.markdown'
101
+ last_response.body.should == File.read(File.join(FIR_ROOT, 'pages', 'products', 'index.markdown'))
102
+ end
103
+ end
104
+ end
105
+
106
+ describe 'POST page/pagename' do
107
+ it 'requires HTTP auth' do
108
+ post '/-admin/pages/index.markdown'
109
+ last_response.status.should == 401
110
+ end
111
+
112
+ it 'does not accept bad user/pass combos' do
113
+ authorize 'johndoe', 'wrong'
114
+ post '/-admin/pages/index.markdown'
115
+ last_response.status.should == 401
116
+ end
117
+
118
+ context 'with balid login' do
119
+ before :each do
120
+ authorize 'johndoe', 'foo'
121
+ end
122
+
123
+ it 'creates the page if none exists' do
124
+ path = File.join(FIR_ROOT, 'pages', 'new-page.markdown')
125
+ begin
126
+ post '/-admin/pages/new-page.markdown', 'content' => 'New page content'
127
+ File.read(path).should == 'New page content'
128
+ ensure
129
+ File.delete(path)
130
+ end
131
+ end
132
+
133
+ it 'overwrites the page if it exists' do
134
+ path = File.join(FIR_ROOT, 'pages', 'new-page.markdown')
135
+ begin
136
+ File.open(path, 'w') do |file|
137
+ file.write 'Old page content'
138
+ end
139
+ post '/-admin/pages/new-page.markdown', 'content' => 'New page content'
140
+ File.read(path).should == 'New page content'
141
+ ensure
142
+ File.delete(path)
143
+ end
144
+ end
145
+
146
+ def assert_cache_cleared_after_post
147
+ cached_path = File.join(FIR_ROOT, 'public', 'cache', 'cached-page.html')
148
+ page_path = File.join(FIR_ROOT, 'pages', 'cached-page.markdown')
149
+ begin
150
+ File.open(cached_path, 'w') do |file|
151
+ file.write 'This is a cached page'
152
+ end
153
+ post '/-admin/pages/cached-page.markdown', 'content' => 'New page content'
154
+ File.exists?(cached_path).should be_false
155
+ ensure
156
+ File.delete(cached_path) if File.exists?(cached_path)
157
+ File.delete(page_path) if File.exists?(page_path)
158
+ end
159
+ end
160
+
161
+ it 'clears the cache if caching is on' do
162
+ Fir.config.perform_caching = true
163
+ assert_cache_cleared_after_post
164
+ end
165
+
166
+ it "clears the cache even if caching is off (in case it's off temporarily)" do
167
+ Fir.config.perform_caching = false
168
+ assert_cache_cleared_after_post
169
+ end
170
+ end
171
+ end
172
+ end
@@ -1,71 +1,98 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
2
 
3
3
  describe 'Fir' do
4
- it 'returns static files in the public directory'
4
+ before :each do
5
+ load_config
6
+ end
7
+
8
+ it 'allows access to the home page' do
9
+ # Basic sanity check
10
+ get '/'
11
+ last_response.should contain('Welcome to Acme Company!')
12
+ end
13
+
14
+ it 'returns static files in the public directory' do
15
+ get '/static.txt'
16
+ last_response.should contain('This is static.txt.')
17
+ end
5
18
 
6
- it 'returns static files in subdirectories of the public directory'
19
+ it 'returns static files in subdirectories of the public directory' do
20
+ get '/stylesheets/style.css'
21
+ last_response.should contain('General typography')
22
+ end
7
23
 
8
- it 'does not allow access to files above the pages directory'
24
+ it 'does not allow access to files above the pages directory' do
25
+ get '/../pages.yml'
26
+ last_response.should_not be_ok
27
+ last_response.should_not include('index')
28
+ end
9
29
 
10
- it 'maps pretty URLs to files in the pages directory'
30
+ it 'maps pretty URLs to files in the pages directory' do
31
+ get '/contact-us'
32
+ last_response.should contain('Contact Us')
33
+ end
11
34
 
12
- it 'maps pretty URLs to files in subdirectories of the pages directory'
35
+ it 'maps pretty URLs to files in subdirectories of the pages directory' do
36
+ get '/products/dynamite'
37
+ last_response.should contain('Dynamite')
38
+ end
13
39
 
14
- it 'returns 404 for bad URLs'
40
+ it 'returns 404 for bad URLs' do
41
+ get '/doesnt-exist'
42
+ last_response.should be_not_found
43
+ last_response.should contain('Sorry, there is nothing at this address.')
44
+ end
15
45
 
16
46
  context 'with caching on' do
17
- it 'uses the cached page for pages in the root directory'
47
+ before :each do
48
+ Fir.config.perform_caching = true
49
+ end
18
50
 
19
- it 'uses the cached page for pages in subdirectories'
51
+ it 'uses the cached page for pages in the root directory' do
52
+ with_cache('index', '<p>This is the cached index.</p>') do
53
+ get '/'
54
+ last_response.should contain('This is the cached index.')
55
+ end
56
+ end
20
57
 
21
- it 'writes to the cache when a page is accessed'
22
- end
23
-
24
- describe 'page_config.yml' do
25
- it 'can override URL mappings'
58
+ it 'uses the cached page for pages in subdirectories' do
59
+ with_cache('products/dynamite', '<p>Dynamite is cached</p>') do
60
+ get '/products/dynamite'
61
+ last_response.should contain('Dynamite is cached')
62
+ end
63
+ end
26
64
 
27
- it 'can specify page titles (if implemented in the layout)'
65
+ it 'writes to the cache when a page is accessed' do
66
+ get '/'
67
+ File.read(File.join(FIR_ROOT, 'public', 'cache', 'index.html')).should contain('Welcome to Acme Company!')
68
+ end
28
69
 
29
- it 'can specify meta descriptions (if implemented in the layout)'
70
+ it 'creates the cache directory if none exists' do
71
+ begin
72
+ cache_path = File.join(FIR_ROOT, 'public', 'cache')
73
+ FileUtils.rm_rf(cache_path)
74
+ get '/'
75
+ File.directory?(cache_path).should be_true
76
+ ensure
77
+ File.delete File.join(cache_path, 'index.html')
78
+ end
79
+ end
30
80
  end
31
81
 
32
- describe 'admin interface' do
33
- it 'returns 404 for bad URLs'
34
-
35
- describe 'GET pages' do
36
- it 'requires HTTP auth'
37
-
38
- it 'does not accept bad user/pass combos'
39
-
40
- it 'returns an XML feed with each directory and page'
82
+ describe 'page_config.yml' do
83
+ it 'can override URL mappings' do
84
+ get '/products/dynamite%2B'
85
+ last_response.should contain('Dynamite Plus!')
41
86
  end
42
87
 
43
- describe 'GET page/pagename' do
44
- it 'requires HTTP auth'
45
-
46
- it 'does not accept bad user/pass combos'
47
-
48
- it 'returns the source code of the page'
49
-
50
- it 'sets Content-Type to text/plain'
51
-
52
- it 'sets Content-Disposition to attachment'
53
-
54
- it 'sets Content-Disposition filename to the source filename'
88
+ it 'can specify page titles (if implemented in the layout)' do
89
+ get '/products'
90
+ last_response.should have_selector('title', :content => 'Products')
55
91
  end
56
92
 
57
- describe 'POST page/pagename' do
58
- it 'requires HTTP auth'
59
-
60
- it 'does not accept bad user/pass combos'
61
-
62
- it 'creates the page if none exists'
63
-
64
- it 'overwrites the page if it exists'
65
-
66
- it 'clears the cache if caching is on'
67
-
68
- it "clears the cache even if caching is off (in case it's off temporarily)"
93
+ it 'can specify meta descriptions (if implemented in the layout)' do
94
+ get '/products'
95
+ last_response.should have_selector('meta[content="Acme Company\'s products"]', :name => 'description')
69
96
  end
70
97
  end
71
98
  end