fir 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.markdown +109 -0
- data/bin/fir +18 -0
- data/example/Rakefile +4 -0
- data/example/boot_example.rb +7 -0
- data/example/config.rb +8 -0
- data/example/config.ru +12 -0
- data/example/layouts/application.html.erb +34 -0
- data/example/menus.yml +6 -0
- data/example/pages.yml +15 -0
- data/example/pages/404.html.erb +1 -0
- data/example/pages/contact-us.markdown +6 -0
- data/example/pages/index.markdown +4 -0
- data/example/pages/products/dynamite-plus.html.erb +7 -0
- data/example/pages/products/dynamite.html.erb +7 -0
- data/example/pages/products/index.markdown +6 -0
- data/example/pages/products/mouse-trap.html.erb +7 -0
- data/example/public/manage-content.html +55 -0
- data/example/public/static.txt +1 -0
- data/example/public/stylesheets/style.css +8 -0
- data/example/tmp/restart.txt +1 -0
- data/example/users.yml +5 -0
- data/lib/fir.rb +17 -0
- data/lib/fir/admin.rb +150 -0
- data/lib/fir/boot.rb +14 -0
- data/lib/fir/helpers.rb +41 -0
- data/lib/fir/pages.rb +178 -0
- data/lib/fir/static.rb +45 -0
- data/lib/fir/tasks.rb +32 -0
- data/lib/fir/util.rb +52 -0
- data/skeleton/Rakefile +4 -0
- data/skeleton/config.rb +8 -0
- data/skeleton/config.ru +12 -0
- data/skeleton/layouts/application.html.erb +34 -0
- data/skeleton/menus.yml +5 -0
- data/skeleton/pages.yml +6 -0
- data/skeleton/pages/404.html.erb +1 -0
- data/skeleton/pages/contact.markdown +2 -0
- data/skeleton/pages/index.markdown +2 -0
- data/skeleton/public/stylesheets/style.css +8 -0
- data/skeleton/tmp/restart.txt +1 -0
- data/spec/fir_spec.rb +71 -0
- data/spec/spec_helper.rb +9 -0
- metadata +159 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Jarrett Colby
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
Fir Isn't Rails
|
2
|
+
===============
|
3
|
+
|
4
|
+
Fir Isn't Rails is a minimalist Ruby framework for small, static sites. It's Rack-based, so it runs on Phusion Passenger.
|
5
|
+
|
6
|
+
Warning: Alpha Release
|
7
|
+
======================
|
8
|
+
|
9
|
+
Fir is still in alpha. It has only been lightly tested. You may very well encounter bugs. If you do, please visit Fir's GitHub repository and tell us what happened.
|
10
|
+
|
11
|
+
Installation
|
12
|
+
============
|
13
|
+
|
14
|
+
sudo gem intall fir
|
15
|
+
fir my_fir_site
|
16
|
+
|
17
|
+
The second command runs the Fir generator, creating a skeleton Fir site in the directory `my_fir_site`.
|
18
|
+
|
19
|
+
Before we set up Passenger, let's test the site with Rackup:
|
20
|
+
|
21
|
+
cd my_fir_site
|
22
|
+
rackup config.ru -p 3000
|
23
|
+
|
24
|
+
The `-p` option tells Rackup which port to listen on. This can be anything you want. (Obviously, if you're already running Rails or anything else on port 3000, you need to give Rack a different port.)
|
25
|
+
|
26
|
+
Open your browser and visit:
|
27
|
+
|
28
|
+
http://localhost:3000
|
29
|
+
|
30
|
+
You should see the home page. If you don't, something is wrong. Please visit Fir's GitHub repository and tell us what happened.
|
31
|
+
|
32
|
+
Rackup is recommended when you're developing your site locally. Press ctrl-c to kill Rackup.
|
33
|
+
|
34
|
+
Now we're ready to make it work with Passenger, which you're encouraged to use in production. At this point, the instructions become a little iffy, because so much depends on your individual system (e.g. how Apache is configured, whether you can `sudo`). If you're on shared hosting, you'll probably need to contact support and have them perform some of these steps for you.
|
35
|
+
|
36
|
+
For now, let's assume you're on a Mac with the default Apache installation.
|
37
|
+
|
38
|
+
If you haven't done so, [install Passenger](http://www.modrails.com/documentation/Users%20guide.html). You should also read "[Deploying a Rack-based Ruby application](http://www.modrails.com/documentation/Users%20guide.html#_deploying_a_rack_based_ruby_application)" in the Passenger Users' Guide.
|
39
|
+
|
40
|
+
In `httpd.conf`, add a virtual host:
|
41
|
+
|
42
|
+
<VirtualHost *:80>
|
43
|
+
ServerName example.com
|
44
|
+
DocumentRoot /absolute/path/to/my_fir_site/public
|
45
|
+
RackBaseURI /
|
46
|
+
<Directory "/absolute/path/to/my_fir_site/public">
|
47
|
+
Options Indexes FollowSymLinks
|
48
|
+
Order Deny,Allow
|
49
|
+
Allow from all
|
50
|
+
</Directory>
|
51
|
+
</VirtualHost>
|
52
|
+
|
53
|
+
Make sure you edit the correct `httpd.conf`! Some systems may have more than one copy of Apache installed.
|
54
|
+
|
55
|
+
Check `httpd.conf` syntax and restart Apache:
|
56
|
+
|
57
|
+
sudo apachectl configtest
|
58
|
+
sudo apachectl graceful
|
59
|
+
|
60
|
+
That should do it.
|
61
|
+
|
62
|
+
Basic Usage
|
63
|
+
===========
|
64
|
+
|
65
|
+
See the [example app](http://github.com/jarrett/fir-example) to get you started.
|
66
|
+
|
67
|
+
## Editing the layout ##
|
68
|
+
|
69
|
+
Each page will be rendered inside `application.html.erb` in the `layouts` directory. (That name was chosen because it will no doubt be familiar to Rails developers.)
|
70
|
+
|
71
|
+
The current version does not support alternate layouts. Such a feature may be added in the future if there's a need.
|
72
|
+
|
73
|
+
## Creating Pages ##
|
74
|
+
|
75
|
+
Pages live in the `pages` directory, or in a subdirectory thereof. If you create a page called `my-page.markdown` in `pages`, Fir will map it to this URL: `http://youdomain.com/my-page`. Similarly, if you create a subdirectory in `pages` called `subdir`, and put `my-page.markdown` in there, this URL will be used: `http://youdomain.com/subdir/my-page`.
|
76
|
+
|
77
|
+
Pages can have the following extensions:
|
78
|
+
|
79
|
+
.html
|
80
|
+
.html.erb
|
81
|
+
.markdown
|
82
|
+
|
83
|
+
Markdown pages get passed through ERB before the Markdown is transformed to HTML. Thus, you can make Markdown pages dynamic. Remember: ERB runs **before** Markdown, so whatever your ERB tags spit out will be interpreted as Markdown.
|
84
|
+
|
85
|
+
## Configuring Pages ##
|
86
|
+
|
87
|
+
`pages.yml` contains all per-page configurations. Page titles, meta descriptions, and other configs can be specified there. Each page config property will be made available to the templates as an instance variable.
|
88
|
+
|
89
|
+
## Mapping URLs ##
|
90
|
+
|
91
|
+
You can override the default URL mapping in `pages.yml`. Just add a `path` property to a page, and the page will be made available under the path you specify.
|
92
|
+
|
93
|
+
## Content Management ##
|
94
|
+
|
95
|
+
Most of us will be perfectly happy managing content by editing the files in the `pages` directory. But for the sake of our clients, Fir offers another way.
|
96
|
+
|
97
|
+
Fir isn't a CMS. Instead, it exposes a minimal, RESTful API for managing content. The idea is that content should be managed with specially-designed Fir clients, rather than through Fir itself. You can see how this works by downloading the [example app](http://github.com/jarrett/fir-example), booting it, and visiting `/manage-content.html` in your browser. The source of that page is available [on GitHub](http://github.com/jarrett/fir-example/blob/master/public/manage-content.html).
|
98
|
+
|
99
|
+
Webserver Support
|
100
|
+
=================
|
101
|
+
|
102
|
+
Fir is designed primarily for Phusion Passenger. As a Rack-based framework, it theoretically supports Mongrel, CGI, FastCGI, and a host of others. But as of this writing, Fir has only been tested with Rackup and Passenger. If you get Fir working with anything else, please visit Fir's GitHub repository and let us know how you did it.
|
103
|
+
|
104
|
+
Unicode and Others
|
105
|
+
==================
|
106
|
+
|
107
|
+
Fir doesn't change the character encoding of your content, so you can technically use any encoding scheme you want. That being said, UTF-8 is recommended. As always, make sure your HTML template correctly identifies the encoding scheme. (See [The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets](http://www.joelonsoftware.com/articles/Unicode.html) for more information.)
|
108
|
+
|
109
|
+
Don't use non-English characters in your URLs. They don't play nice with the filesystem, and they have to be percent-encoded anyway, resulting in ugly URLs. Avoid special characters other than `-` and `_`.
|
data/bin/fir
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift '/Users/jarrettcolby/apps/fir/lib'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'fir'
|
6
|
+
require 'fileutils'
|
7
|
+
include FileUtils
|
8
|
+
|
9
|
+
unless ARGV.length >= 1
|
10
|
+
raise 'Usage: fir path'
|
11
|
+
end
|
12
|
+
|
13
|
+
fir_root = ARGV[0]
|
14
|
+
if File.exists?(fir_root)
|
15
|
+
raise "#{fir_root} already exists! Aborting."
|
16
|
+
end
|
17
|
+
cp_r FIR_SKELETON_ROOT, fir_root
|
18
|
+
puts "Created new Fir site in #{fir_root}"
|
data/example/Rakefile
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# This file is NOT normally part of a Fir deployment. It only exists so that we can
|
2
|
+
# run the example app against the Fir code in this particular gem distribution,
|
3
|
+
# instead of against whatever version of the Fir gem (if any) happens to be installed
|
4
|
+
# on the local system.
|
5
|
+
|
6
|
+
lib_dir = File.expand_path(File.join(File.dirname(__FILE__), '../lib'))
|
7
|
+
puts `rackup config.ru -p 3001 -e "$:.unshift '#{lib_dir}'"`
|
data/example/config.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# This is the main config file for your site. Add any "require" statements or any other global directives here.
|
2
|
+
|
3
|
+
require 'maruku' # Comment out if you don't want to use Markdown
|
4
|
+
|
5
|
+
Fir.config do |config|
|
6
|
+
config.site_name = 'Acme Co.'
|
7
|
+
config.perform_caching = false
|
8
|
+
end
|
data/example/config.ru
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# This file tells Rack how to boot Fir. It's the point of entry for the whole framework.
|
2
|
+
# You shouldn't have to modify this file. If you came here looking for the app's config
|
3
|
+
# file, you're in the wrong place. Look at config.rb instead.
|
4
|
+
|
5
|
+
FIR_ROOT = File.expand_path(File.dirname(__FILE__))
|
6
|
+
|
7
|
+
require 'fir.rb'
|
8
|
+
require 'config.rb'
|
9
|
+
|
10
|
+
# This is where it all begins.
|
11
|
+
# See boot.rb in the Fir gem if you're curious about what happens next.
|
12
|
+
instance_eval(&Fir.boot_proc)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
2
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
3
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
4
|
+
|
5
|
+
<head>
|
6
|
+
<title>
|
7
|
+
<% if @title and !@title.empty? %>
|
8
|
+
<%= h @title %> -
|
9
|
+
<% end %>
|
10
|
+
Acme Co.
|
11
|
+
</title>
|
12
|
+
<meta http-equiv="content-type" content="text/html;charset=utf-8"/>
|
13
|
+
<% if @description and !@description.empty? %>
|
14
|
+
<meta name="description" content="<%= h @page_description %>"/>
|
15
|
+
<% end %>
|
16
|
+
<link href="/stylesheets/style.css" rel="stylesheet" type="text/css"/>
|
17
|
+
</head>
|
18
|
+
|
19
|
+
<body>
|
20
|
+
|
21
|
+
<div id="container">
|
22
|
+
<ul id="top_nav">
|
23
|
+
<% menu('top') do |item, current_page| %>
|
24
|
+
<!-- current_page is true if the menu item links to the current page, false otherwise.
|
25
|
+
It's usually not necessary, since you'll automatically get either an <a> or <span class="selected">.-->
|
26
|
+
<li><%= item %></li>
|
27
|
+
<% end %>
|
28
|
+
</ul>
|
29
|
+
|
30
|
+
<%= yield %>
|
31
|
+
</div>
|
32
|
+
|
33
|
+
</body>
|
34
|
+
</html>
|
data/example/menus.yml
ADDED
data/example/pages.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# You must restart your server after editing this file.
|
2
|
+
|
3
|
+
index:
|
4
|
+
title: Home
|
5
|
+
description: Acme Co. sells all the widgets you need to capture road runners and other desert fauna
|
6
|
+
products/index:
|
7
|
+
title: Products
|
8
|
+
description: Acme Company's products
|
9
|
+
products/dynamite:
|
10
|
+
title: Dynamite
|
11
|
+
products/dynamite-plus:
|
12
|
+
title: Dynamite
|
13
|
+
path: products/dynamite+ # Override the default user-visible path. Note that you need to percent-encode the + in URLs.
|
14
|
+
products/mouse-trap:
|
15
|
+
title: Mouse Trap
|
@@ -0,0 +1 @@
|
|
1
|
+
<h1>Sorry, there is nothing at this address. (404 Not Found)</h1>
|
@@ -0,0 +1,55 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
2
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
3
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
4
|
+
|
5
|
+
<head>
|
6
|
+
<title>Content Management API</title>
|
7
|
+
<meta http-equiv="content-type" content="text/html;charset=utf-8"/>
|
8
|
+
<link href="/stylesheets/style.css" rel="stylesheet" type="text/css"/>
|
9
|
+
<style type="text/css">
|
10
|
+
form { border: 2px solid #a0a0a0; padding: 8px; background: #f0f0f0; }
|
11
|
+
</style>
|
12
|
+
</head>
|
13
|
+
|
14
|
+
<body>
|
15
|
+
|
16
|
+
<div id="container">
|
17
|
+
|
18
|
+
<h1>Content Management API</h1>
|
19
|
+
|
20
|
+
<p>This page exists to demonstrate Fir's content management API. It would normally not be included in a Fir site. Instead, you would use a more user-friendly Fir client.</p>
|
21
|
+
|
22
|
+
<p>Fir exposes a simple RESTful API. It uses HTTP basic authentication. The credentials for this site, as defined in <code>users.yml</code>, are:</p>
|
23
|
+
|
24
|
+
<p><pre>username: johndoe
|
25
|
+
password: foo
|
26
|
+
</pre></p>
|
27
|
+
|
28
|
+
<p>This is why FIR isn't really a CMS—it doesn't provide a user-friendly content management interface. Instead, it exposes a machine-friendly API that's meant to be accessed by remote clients. Anyone can build a Fir client by following the examples on this page.</p>
|
29
|
+
|
30
|
+
<h2>Listing Pages</h2>
|
31
|
+
|
32
|
+
<p>An XML feed of pages is available at:</p>
|
33
|
+
<p><code><a href="/-admin/pages">/-admin/pages</a></code></p>
|
34
|
+
|
35
|
+
<h2>Downloading Pages</h2>
|
36
|
+
|
37
|
+
<p>Download the source code of individual pages from URLs like this:</p>
|
38
|
+
<p><code><a href="/-admin/pages/index.markdown">/-admin/pages/index.markdown</a></code></p>
|
39
|
+
<p><code><a href="/-admin/pages/products/dynamite.html.erb">/-admin/pages/products/dynamite.html.erb</a></code></p>
|
40
|
+
|
41
|
+
<h2>Updating Pages</h2>
|
42
|
+
|
43
|
+
<p>Update page source code by POSTing to the same URLs that are used to download the pages:</p>
|
44
|
+
|
45
|
+
<form action="/-admin/pages/index.markdown" method="post">
|
46
|
+
<p>This form POSTs to:<br/><code>/-admin/pages/index.markdown</code></p>
|
47
|
+
|
48
|
+
<p><textarea name="content" rows="5" cols="80">Whatever you enter here will replace the existing content of index.markdown. This textarea's name is "content."</textarea></p>
|
49
|
+
|
50
|
+
<p><input type="submit" value="Submit"/></p>
|
51
|
+
</form>
|
52
|
+
|
53
|
+
</div><!-- container -->
|
54
|
+
</body>
|
55
|
+
</html>
|
@@ -0,0 +1 @@
|
|
1
|
+
This is static.txt. It exists to prove that you can access static files in the public folder.
|
@@ -0,0 +1,8 @@
|
|
1
|
+
body { font-family: helvetica, arial, verdana, sans-serif; font-size: 83%; text-align: center; }
|
2
|
+
/* General typography */
|
3
|
+
h1 { font-size: 2em; }
|
4
|
+
h2 { font-size: 1.7em; }
|
5
|
+
h3 { font-size: 1.5em; }
|
6
|
+
/* Layout elements */
|
7
|
+
div#container { width: 960px; margin-left: auto; margin-right: auto; text-align: left; }
|
8
|
+
/* Specific pages */
|
@@ -0,0 +1 @@
|
|
1
|
+
Run "touch tmp/restart.txt" to restart Passenger. (This doesn't work if you stared Fir using rackup.)
|
data/example/users.yml
ADDED
data/lib/fir.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'yaml'
|
4
|
+
require 'digest'
|
5
|
+
require 'erb'
|
6
|
+
require 'builder'
|
7
|
+
require 'maruku'
|
8
|
+
|
9
|
+
require 'fir/admin.rb'
|
10
|
+
require 'fir/boot.rb'
|
11
|
+
require 'fir/helpers.rb'
|
12
|
+
require 'fir/pages.rb'
|
13
|
+
require 'fir/static.rb'
|
14
|
+
require 'fir/tasks.rb'
|
15
|
+
require 'fir/util.rb'
|
16
|
+
|
17
|
+
FIR_SKELETON_ROOT = File.join(File.expand_path(File.dirname(__FILE__)), '..', 'skeleton')
|
data/lib/fir/admin.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
module Fir
|
2
|
+
module AdminMiddleware
|
3
|
+
def admin_path?(env)
|
4
|
+
!!env['PATH_INFO'].chomp('/').match(/^\/?-admin/)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class AdminAuth < ::Rack::Auth::Basic
|
9
|
+
include AdminMiddleware
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
super
|
13
|
+
if File.exists?('users.yml')
|
14
|
+
@users = YAML::load(File.read('users.yml'))
|
15
|
+
@users = {} unless @users.is_a?(Hash) # In case of an invalid YAML file
|
16
|
+
else
|
17
|
+
@users = {}
|
18
|
+
end
|
19
|
+
@authenticator = lambda do |username, password|
|
20
|
+
if @users[username]
|
21
|
+
salt = @users[username]['salt']
|
22
|
+
expected = @users[username]['crypted_password']
|
23
|
+
actual = Fir.encrypt_password(password, salt)
|
24
|
+
expected == actual
|
25
|
+
else
|
26
|
+
false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def call(env)
|
32
|
+
if admin_path?(env)
|
33
|
+
super(env)
|
34
|
+
else
|
35
|
+
@app.call(env)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class AdminInterface
|
41
|
+
include AdminMiddleware
|
42
|
+
|
43
|
+
def initialize(app)
|
44
|
+
@app = app
|
45
|
+
@file_server = ::Rack::File.new(File.join(FIR_ROOT, 'public'))
|
46
|
+
end
|
47
|
+
|
48
|
+
def call(env)
|
49
|
+
session = env['rack.session']
|
50
|
+
path = env['PATH_INFO'].chomp('/')
|
51
|
+
if admin_path?(env)
|
52
|
+
match = path.match(/^\/?-admin\/pages\/(.+)/)
|
53
|
+
if match
|
54
|
+
page = Fir.sanitize_page(match[1])
|
55
|
+
page_path = File.join(FIR_ROOT, 'pages', page)
|
56
|
+
if File.exists?(page_path)
|
57
|
+
case env['REQUEST_METHOD'].downcase
|
58
|
+
when 'get'
|
59
|
+
retrieve_page(page_path)
|
60
|
+
when 'post'
|
61
|
+
update_page(page_path, env)
|
62
|
+
else
|
63
|
+
return method_not_allowed('Only GET and POST are allowed')
|
64
|
+
end
|
65
|
+
else
|
66
|
+
not_found
|
67
|
+
end
|
68
|
+
elsif path.match(/^\/?-admin\/pages$/)
|
69
|
+
page_index
|
70
|
+
else
|
71
|
+
not_found
|
72
|
+
end
|
73
|
+
else
|
74
|
+
return @app.call(env)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
protected
|
79
|
+
|
80
|
+
def page_index
|
81
|
+
builder = Builder::XmlMarkup.new(:indent => 2)
|
82
|
+
builder.instruct!
|
83
|
+
xml = builder.pages do |pages|
|
84
|
+
recurse = lambda do |dir, node|
|
85
|
+
dir.each do |entry|
|
86
|
+
if entry[0].chr != '.'
|
87
|
+
path = File.join(dir.path, entry)
|
88
|
+
if File.file?(path)
|
89
|
+
node.page do |page|
|
90
|
+
page.name entry
|
91
|
+
end
|
92
|
+
else # Directory
|
93
|
+
node.folder do |folder|
|
94
|
+
folder.name entry
|
95
|
+
folder.pages do |folder_pages|
|
96
|
+
recurse.call(Dir.new(path), folder_pages)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
recurse.call(Dir.new('pages'), pages)
|
104
|
+
end
|
105
|
+
[200, {'Content-Type' => 'text/xml'}, xml]
|
106
|
+
end
|
107
|
+
|
108
|
+
def retrieve_page(page_path)
|
109
|
+
if File.readable?(page_path)
|
110
|
+
source = File.read(page_path)
|
111
|
+
directory, filename, ext = Fir.split_path(page_path)
|
112
|
+
filename_with_ext = filename + '.' + ext
|
113
|
+
[200, {'Content-Type' => 'text/plain', 'Content-Disposition' => "attachment; #{filename_with_ext}"}, source]
|
114
|
+
else
|
115
|
+
internal_server_error('File exists on disk, but is not readable. Are the permissions wrong?')
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def update_page(page_path, env)
|
120
|
+
if File.writable?(page_path)
|
121
|
+
File.open(page_path, 'w') do |file|
|
122
|
+
request = ::Rack::Request.new(env)
|
123
|
+
file.write(request.POST['content'])
|
124
|
+
end
|
125
|
+
Fir.clear_cache(page_path)
|
126
|
+
[200, {'Content-Type' => 'text/plain'}, "#{page_path} has been saved."]
|
127
|
+
else
|
128
|
+
internal_server_error('File exists on disk, but is not writeable. Are the permissions wrong?')
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
### HTTP Response Codes ###
|
133
|
+
|
134
|
+
def bad_request(explanation = nil)
|
135
|
+
[400, {'Content-Type' => 'text/plain'}, "400 Bad Request #{explanation}" ]
|
136
|
+
end
|
137
|
+
|
138
|
+
def method_not_allowed(explanation = nil)
|
139
|
+
[405, {'Content-Type' => 'text/plain'}, "405 Method Not Allowed #{explanation}" ]
|
140
|
+
end
|
141
|
+
|
142
|
+
def not_found(explanation = nil)
|
143
|
+
[404, {'Content-Type' => 'text/plain'}, "404 Not Found #{explanation}" ]
|
144
|
+
end
|
145
|
+
|
146
|
+
def internal_server_error(explanation = nil)
|
147
|
+
[500, {'Content-Type' => 'text/plain'}, "500 Internal Server Error #{explanation}" ]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
data/lib/fir/boot.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
module Fir
|
2
|
+
def self.boot_proc
|
3
|
+
# This get called in config.ru. Thus, Passenger can eval the contents of config.ru the way it wants to
|
4
|
+
lambda do
|
5
|
+
::Fir.validate_config
|
6
|
+
#use ::Rack::Reloader # Only when developing the Fir gem
|
7
|
+
use ::Rack::ContentLength
|
8
|
+
use ::Fir::Static
|
9
|
+
use ::Fir::AdminAuth
|
10
|
+
use ::Fir::AdminInterface
|
11
|
+
run ::Fir::PageRenderer.new
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/fir/helpers.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module Fir
|
2
|
+
module Helpers
|
3
|
+
include ::ERB::Util
|
4
|
+
|
5
|
+
def absolutize(path)
|
6
|
+
File.join(Fir.config.relative_url_root, path)
|
7
|
+
end
|
8
|
+
|
9
|
+
def content_tag(name, content_or_options = {}, options = {})
|
10
|
+
if content_or_options.is_a?(Hash)
|
11
|
+
options = content_or_options
|
12
|
+
content = nil
|
13
|
+
else
|
14
|
+
content = content_or_options
|
15
|
+
end
|
16
|
+
Builder::XmlMarkup.new.tag!(name, content, options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def link_to(text, url)
|
20
|
+
content_tag('a', text, 'href' => url)
|
21
|
+
end
|
22
|
+
|
23
|
+
class MenuNotDefined < StandardError; end
|
24
|
+
def menu(name, options = {})
|
25
|
+
raise(ArgumentError, 'menu requires a block') unless block_given?
|
26
|
+
options = {:link_to_current => false}.merge(options)
|
27
|
+
items = @menus[name] || raise(MenuNotDefined, "Menu '#{name}' is not defined. Define it in menus.yml.")
|
28
|
+
items.each do |pair|
|
29
|
+
# pair looks like: {'Menu Text' => 'path' } or {'Menu Text' => {'link' => 'path', 'other_option' => 'value'}}
|
30
|
+
text = pair.keys.first
|
31
|
+
item_options = pair[text]
|
32
|
+
path = absolutize((item_options.is_a?(Hash) ? item_options['link'] : item_options) || '')
|
33
|
+
if options[:link_to_current] or path != @request.path_info
|
34
|
+
yield link_to(text, path), true
|
35
|
+
else
|
36
|
+
yield content_tag('span', text, 'class' => 'selected'), false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/fir/pages.rb
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
module Fir
|
2
|
+
module TemplateLoading
|
3
|
+
protected
|
4
|
+
|
5
|
+
def load_template(path, base_dir = 'pages')
|
6
|
+
directory, filename, ext = Fir.split_path(path)
|
7
|
+
path_without_extension = File.join(FIR_ROOT, base_dir, directory, filename)
|
8
|
+
path_and_extension = template_extensions(base_dir).collect do |ext|
|
9
|
+
[path_without_extension + '.' + ext, ext]
|
10
|
+
end.detect do |full_path, ext|
|
11
|
+
File.exists?(full_path)
|
12
|
+
end
|
13
|
+
return nil if path_and_extension.nil?
|
14
|
+
full_path = path_and_extension[0]
|
15
|
+
extension = path_and_extension[1]
|
16
|
+
template_formats[base_dir][extension].new(full_path)
|
17
|
+
end
|
18
|
+
|
19
|
+
def template_extensions(base_dir)
|
20
|
+
template_formats[base_dir].keys
|
21
|
+
end
|
22
|
+
|
23
|
+
# Must return a hash with the following structure. The top-level keys map to directories, e.g. pages and layouts.
|
24
|
+
# The second-level keys are file extensions, and the second-level values are subclasses up TemplateAdapter.
|
25
|
+
def template_formats
|
26
|
+
{
|
27
|
+
'pages' => {'html.erb' => ErbAdapter, 'html' => NoOpAdapter, 'markdown' => MarkdownAdapter},
|
28
|
+
'layouts' => {'html.erb' => ErbAdapter}
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class PageRenderer
|
34
|
+
include TemplateLoading
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
@page_config = yaml_or_empty('pages.yml')
|
38
|
+
@path_mappings = @page_config.inject({}) do |mappings, (page, configs)|
|
39
|
+
mappings[configs['path']] = page if configs['path']
|
40
|
+
mappings
|
41
|
+
end
|
42
|
+
@menus = yaml_or_empty('menus.yml')
|
43
|
+
end
|
44
|
+
|
45
|
+
def call(env)
|
46
|
+
status = 200
|
47
|
+
content_type = 'text/html'
|
48
|
+
body = ''
|
49
|
+
path = ::Rack::Utils.unescape(env['PATH_INFO']).chomp('/').gsub(/^\//, '')
|
50
|
+
path = Fir.sanitize_page(path) # path now looks like 'folder/page-name'
|
51
|
+
if @path_mappings.has_key?(path)
|
52
|
+
path = @path_mappings[path]
|
53
|
+
end
|
54
|
+
template_options = {:menus => @menus, :request => Rack::Request.new(env)}
|
55
|
+
if directory?(path)
|
56
|
+
# This is like Apache's DirectoryIndex: if the request is for
|
57
|
+
# a directory, look for a page called "index" in that directory.
|
58
|
+
path = File.join(path, 'index').gsub(/^\//, '') # path now looks like 'folder/index' or just 'index'
|
59
|
+
end
|
60
|
+
if template = load_template(path)
|
61
|
+
body = template.render_with_layout(template_options.merge(:page_config => page_config(path)))
|
62
|
+
if Fir.config.perform_caching
|
63
|
+
cache_result(path, body)
|
64
|
+
end
|
65
|
+
elsif template = load_template('404')
|
66
|
+
# Page not found. Render 404 template.
|
67
|
+
status = 404
|
68
|
+
body = template.render_with_layout(template_options)
|
69
|
+
else
|
70
|
+
# Page not found, and neither was the 404 template! Render a simple message.
|
71
|
+
content_type = 'text/plain'
|
72
|
+
status = 404
|
73
|
+
body = 'Sorry, there is nothing at this address. (404 Not Found)'
|
74
|
+
end
|
75
|
+
[status, {'Content-Type' => content_type}, body ]
|
76
|
+
end
|
77
|
+
|
78
|
+
protected
|
79
|
+
|
80
|
+
def cache_result(path, body)
|
81
|
+
directory, filename, ext = Fir.split_path(path)
|
82
|
+
path = File.join(FIR_ROOT, 'public/cache', directory, filename + '.html')
|
83
|
+
File.open(path, 'w') do |file|
|
84
|
+
file.write(body)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def directory?(path)
|
89
|
+
File.directory?(File.join('pages', path))
|
90
|
+
end
|
91
|
+
|
92
|
+
def page_config(path)
|
93
|
+
@page_config[path] || {}
|
94
|
+
end
|
95
|
+
|
96
|
+
# Load the YAML file, or return an empty hash if the file can't be loaded
|
97
|
+
def yaml_or_empty(file)
|
98
|
+
(File.exists?(file) and File.readable?(file)) ?
|
99
|
+
YAML::load(File.read(file)) || {} : {}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class TemplateAdapter
|
104
|
+
include TemplateLoading
|
105
|
+
|
106
|
+
def initialize(template_path)
|
107
|
+
# ERB uses the template adapter's bindings, so we don't want to pollute the
|
108
|
+
# namespace any more than we have too. Thus the two preceding underscores.
|
109
|
+
@__path = template_path
|
110
|
+
end
|
111
|
+
|
112
|
+
def render_with_layout(options)
|
113
|
+
wrapper_template.render(options) do
|
114
|
+
# This block will be called where "yield" appears in the wrapper template.
|
115
|
+
# See comment on ErbAdapter#render.
|
116
|
+
render(options)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
protected
|
121
|
+
|
122
|
+
# Returns an instance of ErbAdapter
|
123
|
+
def wrapper_template
|
124
|
+
load_template('application', 'layouts') || raise('layouts/application.html.erb must exist!')
|
125
|
+
end
|
126
|
+
|
127
|
+
def source
|
128
|
+
File.read(@__path)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Passes through the content as-is.
|
133
|
+
# This is desirable for, e.g., .html files.
|
134
|
+
class NoOpAdapter < TemplateAdapter
|
135
|
+
def render(options)
|
136
|
+
source
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class ErbAdapter < TemplateAdapter
|
141
|
+
include ::Fir::Helpers
|
142
|
+
|
143
|
+
# If a block is given, it will be included in ERB's binding. Thus, if the template yields,
|
144
|
+
# the block given to render will be called.
|
145
|
+
def render(options, &block)
|
146
|
+
render_erb(source, options, &block)
|
147
|
+
end
|
148
|
+
|
149
|
+
protected
|
150
|
+
|
151
|
+
def render_erb(__erb_source, __options)
|
152
|
+
if __options[:page_config]
|
153
|
+
__options[:page_config].each do |key, value|
|
154
|
+
instance_variable_set('@' + key, value)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
@menus = __options[:menus]
|
158
|
+
@request = __options[:request]
|
159
|
+
ERB.new(__erb_source).result(binding)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Uses Maruku
|
164
|
+
class MarkdownAdapter < ErbAdapter
|
165
|
+
def render(options)
|
166
|
+
render_markdown(render_erb(source, options), options)
|
167
|
+
end
|
168
|
+
|
169
|
+
protected
|
170
|
+
|
171
|
+
def render_markdown(markdown_source, options)
|
172
|
+
unless defined?(Maruku)
|
173
|
+
raise 'Markdown requires Maruku. "gem install maruku" if necessary, then "require \'maruku\'" in config.rb'
|
174
|
+
end
|
175
|
+
Maruku.new(markdown_source).to_html
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
data/lib/fir/static.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module Fir
|
2
|
+
class Static
|
3
|
+
FORBIDDEN_DIRS = ['.svn', '.git']
|
4
|
+
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
@file_server = ::Rack::File.new(File.join(FIR_ROOT, 'public'))
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
path = Fir.sanitize_page(::Rack::Utils.unescape(env['PATH_INFO']).chomp('/'))
|
12
|
+
method = env['REQUEST_METHOD']
|
13
|
+
if method == 'GET'
|
14
|
+
if static_exists?(path, true)
|
15
|
+
return @file_server.call(env)
|
16
|
+
elsif Fir.config.perform_caching
|
17
|
+
cached_path = File.join('cache', path)
|
18
|
+
# Change PATH_INFO because @file_server uses that variable to find the file
|
19
|
+
if static_exists?(cached_path, false) # Is it a directory?
|
20
|
+
index_path = File.join(cached_path, 'index.html')
|
21
|
+
if static_exists?(index_path, true)
|
22
|
+
env['PATH_INFO'] = index_path
|
23
|
+
return @file_server.call(env)
|
24
|
+
else # If the directory index isn't cached, pass on the request so PageRenderer can look for an index file.
|
25
|
+
return @app.call(env)
|
26
|
+
end
|
27
|
+
elsif static_exists?(cached_path + '.html', true) # Is it a file?
|
28
|
+
env['PATH_INFO'] = cached_path + '.html'
|
29
|
+
return @file_server.call(env)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
return @app.call(env)
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
# If look_for_files is true, use File.file?, else use File.directory?
|
39
|
+
def static_exists?(path, look_for_files)
|
40
|
+
return false unless (path.split('/') & FORBIDDEN_DIRS).empty?
|
41
|
+
path = File.join(@file_server.root, path)
|
42
|
+
(look_for_files ? File.file?(path) : File.directory?(path)) and File.readable?(path)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/fir/tasks.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Fir
|
2
|
+
module Tasks
|
3
|
+
def self.define
|
4
|
+
namespace :users do
|
5
|
+
desc 'Add a user for the content management API. If the user already exists, the password is overwritten. ' +
|
6
|
+
'You must restart your server for the change to take effect. Usage: rake add_user user=username pass=password'
|
7
|
+
task :add do
|
8
|
+
require 'fir'
|
9
|
+
require 'active_support/secure_random'
|
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'
|
12
|
+
end
|
13
|
+
user = ENV['user'] || ENV['USER']
|
14
|
+
pass = ENV['pass'] || ENV['PASS']
|
15
|
+
|
16
|
+
if File.exists?('users.yml')
|
17
|
+
pairs = YAML::load(File.read('users.yml')) || {}
|
18
|
+
else
|
19
|
+
pairs = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
salt = ActiveSupport::SecureRandom.hex(13)
|
23
|
+
pairs[user] = {'crypted_password' => Fir.encrypt_password(pass, salt), 'salt' => salt}
|
24
|
+
|
25
|
+
File.open('users.yml', 'w') do |file|
|
26
|
+
file.write(YAML::dump(pairs))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/fir/util.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Fir
|
2
|
+
# Page is a path such as you would find in a URL, not a local filesystem path
|
3
|
+
def self.clear_cache(page)
|
4
|
+
directory, filename, ext = Fir.split_path(page)
|
5
|
+
path = File.join(FIR_ROOT, 'public/cache', directory, filename + '.html')
|
6
|
+
if File.exists?(path)
|
7
|
+
File.delete(path)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.config
|
12
|
+
@config ||= OpenStruct.new
|
13
|
+
yield @config if block_given?
|
14
|
+
# Default configs go here
|
15
|
+
@config.perform_caching ||= false
|
16
|
+
@config.relative_url_root ||= ''
|
17
|
+
@config
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.encrypt_password(password, salt)
|
21
|
+
crypted_password = password + salt
|
22
|
+
3.times do
|
23
|
+
crypted_password = Digest::SHA512.hexdigest(crypted_password)
|
24
|
+
end
|
25
|
+
crypted_password
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.sanitize_page(page)
|
29
|
+
page.gsub('../', '')
|
30
|
+
end
|
31
|
+
|
32
|
+
# returns [directories, name w/o extension, extension]
|
33
|
+
def self.split_path(path)
|
34
|
+
ext = File.extname(path)
|
35
|
+
dirname = File.dirname(path)
|
36
|
+
dirname = '' if dirname == '.'
|
37
|
+
[dirname, File.basename(path, ext), ext]
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.validate_config
|
41
|
+
unless @config
|
42
|
+
raise 'Fir cannot start unless you call Fir.config in config.rb.'
|
43
|
+
end
|
44
|
+
missing_options = [:site_name, :perform_caching, :relative_url_root].inject([]) do |missing, opt|
|
45
|
+
missing << opt if @config.send(opt).nil?
|
46
|
+
missing
|
47
|
+
end
|
48
|
+
unless missing_options.empty?
|
49
|
+
raise "Fir.config (in config.rb) is missing the following options: #{missing_options.join(', ')}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/skeleton/Rakefile
ADDED
data/skeleton/config.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# This is the main config file for your site. Add any "require" statements or any other global directives here.
|
2
|
+
|
3
|
+
require 'maruku' # Comment out if you don't want to use Markdown
|
4
|
+
|
5
|
+
Fir.config do |config|
|
6
|
+
config.site_name = 'Change me in config.rb'
|
7
|
+
config.perform_caching = false
|
8
|
+
end
|
data/skeleton/config.ru
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# This file tells Rack how to boot Fir. It's the point of entry for the whole framework.
|
2
|
+
# You shouldn't have to modify this file. If you came here looking for the app's config
|
3
|
+
# file, you're in the wrong place. Look at config.rb instead.
|
4
|
+
|
5
|
+
FIR_ROOT = File.expand_path(File.dirname(__FILE__))
|
6
|
+
|
7
|
+
require 'fir.rb'
|
8
|
+
require 'config.rb'
|
9
|
+
|
10
|
+
# This is where it all begins.
|
11
|
+
# See boot.rb in the Fir gem if you're curious about what happens next.
|
12
|
+
instance_eval(&Fir.boot_proc)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
2
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
3
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
4
|
+
|
5
|
+
<head>
|
6
|
+
<title>
|
7
|
+
<% if @title and !@title.empty? %>
|
8
|
+
<%= h @title %> -
|
9
|
+
<% end %>
|
10
|
+
My Site
|
11
|
+
</title>
|
12
|
+
<meta http-equiv="content-type" content="text/html;charset=utf-8"/>
|
13
|
+
<% if @description and !@description.empty? %>
|
14
|
+
<meta name="description" content="<%= h @page_description %>"/>
|
15
|
+
<% end %>
|
16
|
+
<link href="/stylesheets/style.css" rel="stylesheet" type="text/css"/>
|
17
|
+
</head>
|
18
|
+
|
19
|
+
<body>
|
20
|
+
|
21
|
+
<div id="container">
|
22
|
+
<ul id="top_nav">
|
23
|
+
<% menu('main') do |item, current_page| %>
|
24
|
+
<!-- current_page is true if the menu item links to the current page, false otherwise.
|
25
|
+
It's usually not necessary, since you'll automatically get either an <a> or <span class="selected">.-->
|
26
|
+
<li><%= item %></li>
|
27
|
+
<% end %>
|
28
|
+
</ul>
|
29
|
+
|
30
|
+
<%= yield %>
|
31
|
+
</div>
|
32
|
+
|
33
|
+
</body>
|
34
|
+
</html>
|
data/skeleton/menus.yml
ADDED
data/skeleton/pages.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
<h1>Sorry, there is nothing at this address. (404 Not Found)</h1>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
body { font-family: helvetica, arial, verdana, sans-serif; font-size: 83%; text-align: center; }
|
2
|
+
/* General typography */
|
3
|
+
h1 { font-size: 2em; }
|
4
|
+
h2 { font-size: 1.7em; }
|
5
|
+
h3 { font-size: 1.5em; }
|
6
|
+
/* Layout elements */
|
7
|
+
div#container { width: 960px; margin-left: auto; margin-right: auto; text-align: left; }
|
8
|
+
/* Specific pages */
|
@@ -0,0 +1 @@
|
|
1
|
+
Run "touch tmp/restart.txt" to restart Passenger. (This doesn't work if you stared Fir using rackup.)
|
data/spec/fir_spec.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe 'Fir' do
|
4
|
+
it 'returns static files in the public directory'
|
5
|
+
|
6
|
+
it 'returns static files in subdirectories of the public directory'
|
7
|
+
|
8
|
+
it 'does not allow access to files above the pages directory'
|
9
|
+
|
10
|
+
it 'maps pretty URLs to files in the pages directory'
|
11
|
+
|
12
|
+
it 'maps pretty URLs to files in subdirectories of the pages directory'
|
13
|
+
|
14
|
+
it 'returns 404 for bad URLs'
|
15
|
+
|
16
|
+
context 'with caching on' do
|
17
|
+
it 'uses the cached page for pages in the root directory'
|
18
|
+
|
19
|
+
it 'uses the cached page for pages in subdirectories'
|
20
|
+
|
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'
|
26
|
+
|
27
|
+
it 'can specify page titles (if implemented in the layout)'
|
28
|
+
|
29
|
+
it 'can specify meta descriptions (if implemented in the layout)'
|
30
|
+
end
|
31
|
+
|
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'
|
41
|
+
end
|
42
|
+
|
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'
|
55
|
+
end
|
56
|
+
|
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)"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fir
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- jarrett
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-03-01 00:00:00 -06:00
|
18
|
+
default_executable: fir
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 1
|
29
|
+
- 2
|
30
|
+
- 9
|
31
|
+
version: 1.2.9
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: rack
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 1
|
43
|
+
- 0
|
44
|
+
- 1
|
45
|
+
version: 1.0.1
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: builder
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 2
|
57
|
+
- 1
|
58
|
+
- 2
|
59
|
+
version: 2.1.2
|
60
|
+
type: :runtime
|
61
|
+
version_requirements: *id003
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: maruku
|
64
|
+
prerelease: false
|
65
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
segments:
|
70
|
+
- 0
|
71
|
+
- 6
|
72
|
+
- 0
|
73
|
+
version: 0.6.0
|
74
|
+
type: :runtime
|
75
|
+
version_requirements: *id004
|
76
|
+
description: Rack-based, Passenger-compatible, with pretty URLs and simple HTTP API for managing content
|
77
|
+
email: jarrett@uchicago.edu
|
78
|
+
executables:
|
79
|
+
- fir
|
80
|
+
extensions: []
|
81
|
+
|
82
|
+
extra_rdoc_files:
|
83
|
+
- LICENSE
|
84
|
+
- README.markdown
|
85
|
+
files:
|
86
|
+
- bin/fir
|
87
|
+
- example/Rakefile
|
88
|
+
- example/boot_example.rb
|
89
|
+
- example/config.rb
|
90
|
+
- example/config.ru
|
91
|
+
- example/layouts/application.html.erb
|
92
|
+
- example/menus.yml
|
93
|
+
- example/pages.yml
|
94
|
+
- example/pages/404.html.erb
|
95
|
+
- example/pages/contact-us.markdown
|
96
|
+
- example/pages/index.markdown
|
97
|
+
- example/pages/products/dynamite-plus.html.erb
|
98
|
+
- example/pages/products/dynamite.html.erb
|
99
|
+
- example/pages/products/index.markdown
|
100
|
+
- example/pages/products/mouse-trap.html.erb
|
101
|
+
- example/public/manage-content.html
|
102
|
+
- example/public/static.txt
|
103
|
+
- example/public/stylesheets/style.css
|
104
|
+
- example/tmp/restart.txt
|
105
|
+
- example/users.yml
|
106
|
+
- lib/fir.rb
|
107
|
+
- lib/fir/admin.rb
|
108
|
+
- lib/fir/boot.rb
|
109
|
+
- lib/fir/helpers.rb
|
110
|
+
- lib/fir/pages.rb
|
111
|
+
- lib/fir/static.rb
|
112
|
+
- lib/fir/tasks.rb
|
113
|
+
- lib/fir/util.rb
|
114
|
+
- skeleton/Rakefile
|
115
|
+
- skeleton/config.rb
|
116
|
+
- skeleton/config.ru
|
117
|
+
- skeleton/layouts/application.html.erb
|
118
|
+
- skeleton/menus.yml
|
119
|
+
- skeleton/pages.yml
|
120
|
+
- skeleton/pages/404.html.erb
|
121
|
+
- skeleton/pages/contact.markdown
|
122
|
+
- skeleton/pages/index.markdown
|
123
|
+
- skeleton/public/stylesheets/style.css
|
124
|
+
- skeleton/tmp/restart.txt
|
125
|
+
- LICENSE
|
126
|
+
- README.markdown
|
127
|
+
has_rdoc: true
|
128
|
+
homepage: http://github.com/jarrett/fir
|
129
|
+
licenses: []
|
130
|
+
|
131
|
+
post_install_message:
|
132
|
+
rdoc_options:
|
133
|
+
- --charset=UTF-8
|
134
|
+
require_paths:
|
135
|
+
- lib
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
segments:
|
141
|
+
- 0
|
142
|
+
version: "0"
|
143
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
144
|
+
requirements:
|
145
|
+
- - ">="
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
segments:
|
148
|
+
- 0
|
149
|
+
version: "0"
|
150
|
+
requirements: []
|
151
|
+
|
152
|
+
rubyforge_project:
|
153
|
+
rubygems_version: 1.3.6
|
154
|
+
signing_key:
|
155
|
+
specification_version: 3
|
156
|
+
summary: A minimalist Ruby framework for small, static sites
|
157
|
+
test_files:
|
158
|
+
- spec/fir_spec.rb
|
159
|
+
- spec/spec_helper.rb
|