fir 0.0.9 → 0.0.10

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.
@@ -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