stoor 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,17 +1,19 @@
1
- This is an example of fronting Gollum with a small Sinatra app that authorizes against GitHub.
1
+ Stoor provides a Gollum (wiki) server with a few other bells and whistles, such as authentication against GitHub OAuth.
2
2
 
3
3
  ## Rationale
4
4
 
5
5
  In our environment, the contents of our wiki are confidential and should not be pushed up to GitHub. We keep the wiki on
6
6
  a local machine (behind a firewall), and put the wiki contents into a directory that is sync'd to box.com. (box.com is useful for confidential
7
- data, because they will sign a HIPAA BAA.) Meanwhile, we'd like to authorize access by some means, so we use GitHub Oauth,
7
+ data, because they will sign a HIPAA BAA.) Meanwhile, we'd like to authorize access by some means: we like GitHub Oauth,
8
8
  so that we can constrain access by GitHub Organization Team membership.
9
9
 
10
10
  ## Requirements
11
11
 
12
12
  Ruby 1.9.2 or greater.
13
13
 
14
- Unfortunately, this will no longer work on Ruby 1.8.7, because `gollum-lib` now wants Nokogiri 1.6.0 ([see?](https://github.com/gollum/gollum-lib/commit/eeb0a4a036001c7621d173e7152b91ed02b21ed0#commitcomment-4170065)), and
14
+ An operating system other than Windows (because Gollum doesn't work on Windows, because grit doesn't work on Windows . . .).
15
+
16
+ Unfortunately, Stoor will no longer work on Ruby 1.8.7, because `gollum-lib` now wants Nokogiri 1.6.0 ([see?](https://github.com/gollum/gollum-lib/commit/eeb0a4a036001c7621d173e7152b91ed02b21ed0#commitcomment-4170065)), and
15
17
  1.8.7 isn't supported. That's too bad, because it was nice that this would work on the system Ruby on a Mac.
16
18
 
17
19
  ## Setup
@@ -37,9 +39,6 @@ the values for the GitHub commit will be what you see in `git config -l`).
37
39
 
38
40
  The `stoor` command is a thin wrapper around the `thin` web server, and takes all `thin` options (`-p <port>`, etc.).
39
41
 
40
- If you get the error `Gollum::InvalidGitRepositoryError` it means that you didn't change your directory to
41
- a git repo.
42
-
43
42
  If you don't have a repo yet for your wiki . . .
44
43
 
45
44
  mkdir mywiki
@@ -49,9 +48,9 @@ If you don't have a repo yet for your wiki . . .
49
48
 
50
49
  ### Specify the Wiki repo location
51
50
 
52
- WIKI_PATH=/Users/admin/wiki stoor
51
+ STOOR_WIKI_PATH=/Users/admin/wiki stoor
53
52
 
54
- The `WIKI_PATH` environment variable provides for locating the wiki contents in a differet repo from the
53
+ The `STOOR_WIKI_PATH` environment variable provides for locating the wiki contents in a differet repo from the
55
54
  Stoor application. It is strongly advised that you do this so that you can keep your wiki code and wiki
56
55
  content separate.
57
56
 
@@ -59,15 +58,20 @@ content separate.
59
58
 
60
59
  Require authorization via GitHub to the GitHub application with the given client id and secret
61
60
 
62
- GITHUB_CLIENT_ID=780ec06a331b4f61a345 GITHUB_CLIENT_SECRET=f1e5439aff166c34f707747120acbf66ef233fc2 stoor
61
+ STOOR_GITHUB_CLIENT_ID=780ec06a331b4f61a345 STOOR_GITHUB_CLIENT_SECRET=f1e5439aff166c34f707747120acbf66ef233fc2 stoor
63
62
 
64
63
  Access to the wiki will first run through GitHub OAuth against the app specified by the id and secret. For information
65
64
  on setting up an application in GitHub and obtaining its id and secret, see <https://github.com/settings/applications/new>.
66
- If you are running Stoor on localhost with Rackup, the typical settings would be:
65
+ If you are running Stoor on localhost with the `stoor` command, the typical settings would be:
67
66
 
68
- Application Name | Main URL | Callback URL
69
- --- | --- |
70
- YourAppName | http://localhost:3000 | http://localhost:3000/auth/github/callback
67
+ <table>
68
+ <tr>
69
+ <th> Application Name </th> <th> Main URL </th> <th> Callback URL </th>
70
+ </tr>
71
+ <tr>
72
+ <td> YourAppName </td> <td> http://localhost:3000 </td><td> http://localhost:3000/auth/github/callback </td>
73
+ </tr>
74
+ </table>
71
75
 
72
76
  **NOTE:** No matter what your domain and port, the callback path must be `/auth/github/callback`.
73
77
 
@@ -78,11 +82,11 @@ application settings.
78
82
 
79
83
  If there is more than one email associated with the GitHub user, prefer the one from the specified domain (otherwise the first email will be used)
80
84
 
81
- GITHUB_EMAIL_DOMAIN=7fff.com GITHUB_CLIENT_ID=780ec06a331b4f61a345 GITHUB_CLIENT_SECRET=f1e5439aff166c34f707747120acbf66ef233fc2 stoor
85
+ STOOR_GITHUB_EMAIL_DOMAIN=7fff.com STOOR_GITHUB_CLIENT_ID=780ec06a331b4f61a345 STOOR_GITHUB_CLIENT_SECRET=f1e5439aff166c34f707747120acbf66ef233fc2 stoor
82
86
 
83
87
  ### Require GitHub team
84
88
 
85
- GITHUB_TEAM_ID=11155 GITHUB_CLIENT_ID=780ec06a331b4f61a345 GITHUB_CLIENT_SECRET=f1e5439aff166c34f707747120acbf66ef233fc2 stoor
89
+ STOOR_GITHUB_TEAM_ID=11155 STOOR_GITHUB_CLIENT_ID=780ec06a331b4f61a345 STOOR_GITHUB_CLIENT_SECRET=f1e5439aff166c34f707747120acbf66ef233fc2 stoor
86
90
 
87
91
  If the user is not a member of the specified team, they aren't allowed access.
88
92
 
@@ -105,7 +109,7 @@ If the user is not a member of the specified team, they aren't allowed access.
105
109
  ## How I run it
106
110
 
107
111
  I like having my own personal wiki. Since Apache is ubiquitous on Macs, I run the Wiki with configuration in `/etc/apache2/httpd.conf`,
108
- ~~~and just use my system Ruby~~~ some Ruby provided by rbenv, and Passenger.
112
+ and some Ruby provided by rbenv, and Passenger.
109
113
 
110
114
  I create an extra name for 127.0.0.1 in `/etc/hosts` such as `wiki.local`. Then:
111
115
 
@@ -121,12 +125,12 @@ Then in `/etc/apache2/httpd.conf`:
121
125
  NameVirtualHost *:80
122
126
 
123
127
  <VirtualHost *:80>
124
- SetEnv GITHUB_CLIENT_ID 780ec06a331b4f61a345
125
- SetEnv GITHUB_CLIENT_SECRET f1e5439aff166c34f707747120acbf66ef233fc2
126
- SetEnv GITHUB_EMAIL_DOMAIN 7fff.com
128
+ SetEnv STOOR_GITHUB_CLIENT_ID 780ec06a331b4f61a345
129
+ SetEnv STOOR_GITHUB_CLIENT_SECRET f1e5439aff166c34f707747120acbf66ef233fc2
130
+ SetEnv STOOR_GITHUB_EMAIL_DOMAIN 7fff.com
127
131
  SetEnv STOOR_DOMAIN wiki.local
128
132
  SetEnv STOOR_EXPIRE_AFTER 60
129
- SetEnv WIKI_PATH /Users/jgn/Dropbox/wiki
133
+ SetEnv STOOR_WIKI_PATH /Users/jgn/Dropbox/wiki
130
134
  ServerName wiki.local
131
135
  DocumentRoot "/opt/boxen/rbenv/versions/1.9.2-p320/lib/ruby/gems/1.9.1/gems/stoor-0.1.4/public"
132
136
  <Directory "/opt/boxen/rbenv/versions/1.9.2-p320/lib/ruby/gems/1.9.1/gems/stoor-0.1.4/public">
data/bin/stoor CHANGED
@@ -4,7 +4,7 @@ require 'thin'
4
4
 
5
5
  config_ru = File.expand_path(File.join(File.dirname(__FILE__), '..', 'config.ru'))
6
6
 
7
- ENV['WIKI_PATH'] ||= '.'
7
+ ENV['STOOR_WIKI_PATH'] ||= '.'
8
8
 
9
9
  ARGV.unshift config_ru
10
10
  ARGV.unshift '-R'
data/config.ru CHANGED
@@ -23,6 +23,11 @@ module Rack
23
23
  end
24
24
 
25
25
  ENV['RACK_ENV'] ||= 'development'
26
+
27
+ domain = ENV['STOOR_DOMAIN'] || 'localhost'
28
+ secret = ENV['STOOR_SECRET'] || 'stoor'
29
+ expire_after = (ENV['STOOR_EXPIRE_AFTER'] || '3600').to_i
30
+
26
31
  log_frag = "#{File.dirname(__FILE__)}/log/#{ENV['RACK_ENV']}"
27
32
  access_logger = Logger.new("#{log_frag}_access.log")
28
33
  access_logger.instance_eval do
@@ -32,24 +37,53 @@ access_logger.level = Logger::INFO
32
37
  log_stream = File.open("#{log_frag}.log", 'a+')
33
38
  log_stream.sync = true
34
39
 
35
- domain = ENV['STOOR_DOMAIN'] || 'localhost'
36
- secret = ENV['STOOR_SECRET'] || 'stoor'
37
- expire_after = (ENV['STOOR_EXPIRE_AFTER'] || '3600').to_i
38
-
39
- wiki_path = ENV['WIKI_PATH_IN_USE'] = ENV['WIKI_PATH'] || File.expand_path(File.dirname(__FILE__))
40
+ gollum_path = ENV['STOOR_WIKI_PATH'] || File.expand_path(File.dirname(__FILE__))
41
+ repo_exists = true
42
+ begin
43
+ Gollum::Wiki.new(gollum_path)
44
+ rescue Gollum::InvalidGitRepositoryError
45
+ repo_exists = false
46
+ message = "Sorry, #{gollum_path} is not a git repository; you might try `cd #{gollum_path}; git init .`."
47
+ rescue NameError
48
+ repo_exists = false
49
+ message = "Sorry, #{gollum_path} doesn't exist; set the environment variable STOOR_WIKI_PATH to point to a git repository."
50
+ end
40
51
 
52
+ use Rack::Session::Cookie, :domain => domain, :key => 'rack.session', :secret => secret, :expire_after => expire_after
41
53
  use Rack::CommonLogger, access_logger
42
54
  use Stoor::Logger, log_stream, Logger::INFO
43
- use Rack::Session::Cookie, :domain => domain, :key => 'rack.session', :secret => secret, :expire_after => expire_after
55
+ if repo_exists
56
+ use Stoor::GithubAuth
57
+ use Stoor::GitConfig, gollum_path
58
+ use Stoor::TransformContent,
59
+ pass_condition: ->(request) { request.session['gollum.author'].nil? },
60
+ regexp: /(<div id="footer">)(.*?)(<\/div>)/im,
61
+ before: ->(request) do
62
+ <<-HTML
63
+ <div style="float: left;">
64
+ HTML
65
+ end,
66
+ after: ->(request) do
67
+ <<-HTML
68
+ </div>
69
+ <div style="float: right;">
70
+ <p style="text-align: right; font-size: .9em; line-height: 1.6em; color: #999; margin: 0.9em 0;">
71
+ Commiting as <b>#{request.session['gollum.author'][:name]}</b> (#{request.session['gollum.author'][:email]})#{" | <a href='/logout'>Logout</a>" if request.session['stoor.github.authorized']}
72
+ </p>
73
+ </div>
74
+ HTML
75
+ end
76
+ if ENV['STOOR_WIDE']
77
+ use Stoor::TransformContent,
78
+ regexp: /<body>/,
79
+ after: '<style type="text/css">#wiki-wrapper { width: 90%; } .markdown-body table { width: 100%; }</style>'
80
+ end
44
81
 
45
- use Stoor::GithubAuth
46
- use Stoor::GitConfig
47
- use Stoor::Decorate
48
- if ENV['STOOR_WIDE']
49
- use Stoor::AddAfter, /<body>/, '<style type="text/css">#wiki-wrapper { width: 90%; } .markdown-body table { width: 100%; }</style>'
82
+ Precious::App.set(:gollum_path, gollum_path)
83
+ Precious::App.set(:default_markup, :markdown)
84
+ Precious::App.set(:wiki_options, { :universal_toc =>false })
85
+ run Precious::App
86
+ else
87
+ run Proc.new { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ message ] ] }
88
+ puts message
50
89
  end
51
-
52
- Precious::App.set(:gollum_path, wiki_path)
53
- Precious::App.set(:default_markup, :markdown)
54
- Precious::App.set(:wiki_options, { :universal_toc =>false })
55
- run Precious::App
data/lib/stoor.rb CHANGED
@@ -1,8 +1,7 @@
1
1
  require 'stoor/logger'
2
2
  require 'stoor/github_auth'
3
3
  require 'stoor/git_config'
4
- require 'stoor/decorate'
5
- require 'stoor/add_after'
4
+ require 'stoor/transform_content'
6
5
  require 'stoor/views/layout'
7
6
  require 'stoor/views/logout'
8
7
 
@@ -1,24 +1,26 @@
1
1
  module Stoor
2
2
  class GitConfig
3
- def initialize(app); @app = app; end
3
+ attr_reader :repo_path
4
+
5
+ def initialize(app, repo_path = nil)
6
+ @app, @repo_path = app, repo_path
7
+ end
4
8
 
5
9
  def call(env)
6
10
  @request = Rack::Request.new(env)
7
11
  unless @request.session['gollum.author']
8
- if ENV['WIKI_PATH_IN_USE']
9
- if name = git_option_value('user.name')
10
- if email = git_option_value('user.email')
11
- @request.session['gollum.author'] = { :name => name, :email => email }
12
- end
13
- end
12
+ if repo_path && (name = git_option_value('user.name')) && (email = git_option_value('user.email'))
13
+ @request.session['gollum.author'] = { :name => name, :email => email }
14
14
  end
15
15
  end
16
16
  @app.call(env)
17
17
  end
18
18
 
19
+ private
20
+
19
21
  def git_option_value(option)
20
- `cd #{ENV['WIKI_PATH_IN_USE']} && git config --get #{option}`.strip
21
- rescue
22
+ @repo ||= Grit::Repo.new(repo_path)
23
+ @repo.config[option]
22
24
  end
23
25
  end
24
26
  end
@@ -2,9 +2,9 @@ module Stoor
2
2
  class GithubAuth < Sinatra::Base
3
3
 
4
4
  set :github_options, {
5
- :scopes => "user,user:email",
6
- :client_id => ENV['GITHUB_CLIENT_ID'],
7
- :secret => ENV['GITHUB_CLIENT_SECRET']
5
+ :scopes => "user:email",
6
+ :client_id => ENV['STOOR_GITHUB_CLIENT_ID'],
7
+ :secret => ENV['STOOR_GITHUB_CLIENT_SECRET']
8
8
  }
9
9
 
10
10
  register Sinatra::Auth::Github
@@ -16,23 +16,23 @@ module Stoor
16
16
  end
17
17
 
18
18
  get '/*' do
19
- ENV['GITHUB_AUTHORIZED'] = nil
19
+ session['stoor.github.authorized'] = nil
20
20
 
21
- pass unless ENV['GITHUB_CLIENT_ID'] && ENV['GITHUB_CLIENT_SECRET']
21
+ pass unless ENV['STOOR_GITHUB_CLIENT_ID'] && ENV['STOOR_GITHUB_CLIENT_SECRET']
22
22
 
23
23
  pass if request.path_info =~ /\./
24
24
 
25
25
  authenticate!
26
- if ENV['GITHUB_TEAM_ID']
27
- github_team_authenticate!(ENV['GITHUB_TEAM_ID'])
26
+ if ENV['STOOR_GITHUB_TEAM_ID']
27
+ github_team_authenticate!(ENV['STOOR_GITHUB_TEAM_ID'])
28
28
  end
29
29
 
30
- ENV['GITHUB_AUTHORIZED'] = "yes"
30
+ session['stoor.github.authorized'] = 'yes'
31
31
 
32
32
  email = nil
33
33
  emails = github_user.api.emails
34
- if ENV['GITHUB_EMAIL_DOMAIN']
35
- email = emails.find { |e| e =~ /#{ENV['GITHUB_EMAIL_DOMAIN']}/ }
34
+ if ENV['STOOR_GITHUB_EMAIL_DOMAIN']
35
+ email = emails.find { |e| e =~ /#{ENV['STOOR_GITHUB_EMAIL_DOMAIN']}/ }
36
36
  end
37
37
  email ||= emails.first
38
38
  session['gollum.author'] = { :name => github_user.name, :email => email }
@@ -0,0 +1,87 @@
1
+ require 'rack'
2
+
3
+ module Stoor
4
+ class TransformContent
5
+ include Rack::Utils
6
+
7
+ attr_reader :status, :headers, :request
8
+ attr_reader :app, :regexp, :content_type, :pass_condition
9
+
10
+ def initialize(app, options)
11
+ @app = app
12
+ @regexp = options[:regexp]
13
+ @before = options[:before]
14
+ @after = options[:after]
15
+ @content_type = options[:content_type] || 'text/html'
16
+ @pass_condition = options[:pass_condition] || ->(env) { false }
17
+ end
18
+
19
+ def call(env)
20
+ @status, @headers, response = @app.call(env)
21
+ @request = Rack::Request.new(env)
22
+
23
+ if pass_condition.call(request)
24
+ if request.logger
25
+ request.logger.info "Skipping -- because pass condition met"
26
+ end
27
+ else
28
+ @headers = HeaderHash.new(@headers)
29
+ if has_body && not_encoded && headers['content-type'] && headers['content-type'].index(content_type)
30
+ content = Array(response).join('')
31
+ if match_data = content.match(regexp)
32
+ new_body = interpolate(match_data)
33
+ headers['Content-Length'] = new_body.bytesize.to_s
34
+ return [status, headers, [new_body]]
35
+ end
36
+ end
37
+ end
38
+
39
+ [status, headers, response]
40
+ end
41
+
42
+ private
43
+
44
+ def has_body
45
+ !STATUS_WITH_NO_ENTITY_BODY.include?(status)
46
+ end
47
+
48
+ def not_encoded
49
+ !headers['transfer-encoding']
50
+ end
51
+
52
+ def interpolate(match_data)
53
+ # regexp: /<body>/
54
+ # result: before<body>after
55
+ # regexp: /(<div>)(.*?)(<\/div)
56
+ # result: <div>beforezzzafter</div>
57
+ "".tap do |s|
58
+ s << match_data.pre_match
59
+ if match_data.size == 1
60
+ s << before + match_data[0] + after
61
+ elsif match_data.size == 4
62
+ s << match_data[1] + before + match_data[2] + after + match_data[3]
63
+ else
64
+ # Might just set the result to content?
65
+ raise "Unexpected number of captures in #{match_data.regexp}"
66
+ end
67
+ s << match_data.post_match
68
+ end
69
+ end
70
+
71
+ def before
72
+ freshen(@before)
73
+ end
74
+
75
+ def after
76
+ freshen(@after)
77
+ end
78
+
79
+ def freshen(e)
80
+ if e.respond_to? :call
81
+ e.call(request)
82
+ else
83
+ e.to_s
84
+ end
85
+ end
86
+ end
87
+ end
data/lib/stoor/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Stoor
2
- VERSION = '0.1.4'
2
+ VERSION = '0.1.5'
3
3
  end
@@ -0,0 +1,18 @@
1
+ require 'stoor/git_config'
2
+ require 'spec_helper'
3
+ require 'grit'
4
+ require 'repo'
5
+
6
+ module Stoor
7
+ describe GitConfig do
8
+ let(:inner_app) { ->(env) { [200, { }, []] } }
9
+ let(:app) { GitConfig.new(inner_app, 'repo') }
10
+
11
+ include_context 'repo'
12
+
13
+ it 'finds git config user.name and user.email and puts them into the session under the key gollum.author' do
14
+ get '/'
15
+ expect(last_request.env['rack.session']['gollum.author']).to eq(name: 'Mortimer Snerd', email: 'snerd@example.com')
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+ require 'sinatra'
3
+ require 'mustache'
4
+ require 'sinatra/auth/github'
5
+ require 'sinatra/auth/github/test/test_helper'
6
+ require 'gollum/app'
7
+ require 'stoor/github_auth'
8
+ require 'stoor/views/layout'
9
+ require 'stoor/views/logout'
10
+ require 'repo'
11
+
12
+ module Stoor
13
+
14
+ # Follows https://github.com/atmos/sinatra_auth_github/blob/master/spec/login_spec.rb
15
+ # To run these specs, define a testing app at https://github.com/settings/applications/new,
16
+ # with a callback URL such as http://localhost:9393/auth/github/callback
17
+ # Then
18
+ # STOOR_GITHUB_CLIENT_ID=xxx STOOR_GITHUB_CLIENT_SECRET=yyy stoor -p 9393
19
+ # Authenticate.
20
+ # Now you can test:
21
+ # STOOR_GITHUB_CLIENT_ID=xxx STOOR_GITHUB_CLIENT_SECRET=yyy STOOR_TESTING_USER=jgn bundle exec rspec
22
+
23
+ describe "Logged in users" do
24
+ include Stoor::Test::Helper
25
+
26
+ let(:inner_app) { ->(env) { [200, { }, [ 'protected info' ]] } }
27
+ let(:sessions) { Rack::Session::Cookie.new(inner_app, secret: 'boo') }
28
+ let(:app) { GithubAuth.new(sessions) }
29
+
30
+ include_context 'repo'
31
+ ENV['STOOR_WIKI_PATH'] = './repo'
32
+
33
+ before do
34
+ Stoor::GithubAuth.send(:enable, :sessions)
35
+ @user = make_user(ENV['STOOR_TESTING_USER'], [ 'effie@example.com', 'effie@7fff.com', 'john@7fff.com' ])
36
+ login_as @user
37
+ end
38
+
39
+ it 'Shows the home page' do
40
+ get '/'
41
+ expect(last_response.body).to match('protected info')
42
+ end
43
+
44
+ it 'Sets the gollum.user with the first email' do
45
+ get '/'
46
+ expect(last_request.env['rack.session']['gollum.author']).to eq(name: 'Effie Klinker', email: 'effie@example.com')
47
+ end
48
+
49
+ it 'Sets the gollum.user according to domain if specified' do
50
+ save = ENV['STOOR_GITHUB_EMAIL_DOMAIN']
51
+ ENV['STOOR_GITHUB_EMAIL_DOMAIN'] = '7fff.com'
52
+ get '/'
53
+ expect(last_request.env['rack.session']['gollum.author']).to eq(name: 'Effie Klinker', email: 'effie@7fff.com')
54
+ ENV['STOOR_GITHUB_EMAIL_DOMAIN'] = save
55
+ end
56
+
57
+ it 'logs the user out' do
58
+ get '/'
59
+ get '/logout'
60
+ expect(last_response.status).to eql(200)
61
+ expect(last_response.body).to match("You're logged out")
62
+
63
+ get '/'
64
+ expect(last_response.status).to eql(302)
65
+ expect(last_response.headers['Location']).to match(/^https:\/\/github\.com\/login\/oauth\/authorize/)
66
+ end
67
+
68
+ it 'shows the securocat when github returns an oauth error' do
69
+ get '/auth/github/callback?error=redirect_uri_mismatch'
70
+ follow_redirect!
71
+ expect(last_response.body).to match(/securocat\.png/)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,146 @@
1
+ require 'stoor/transform_content'
2
+ require 'spec_helper'
3
+
4
+ module Stoor
5
+
6
+ describe TransformContent, 'content-type matching' do
7
+ context 'default with text/plain response' do
8
+ let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/plain' }, '<body>'] } }
9
+ let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text') }
10
+
11
+ it "skips" do
12
+ get '/'
13
+ expect(last_response.body).to eq('<body>')
14
+ end
15
+ end
16
+
17
+ context 'default with text/html response' do
18
+ let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
19
+ let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text') }
20
+
21
+ it "processes" do
22
+ get '/'
23
+ expect(last_response.body).to eq('<body>after-text')
24
+ end
25
+ end
26
+
27
+ context 'specifying text/plain with text/html response' do
28
+ let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
29
+ let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text', content_type: 'text/plain') }
30
+
31
+ it "skips" do
32
+ get '/'
33
+ expect(last_response.body).to eq('<body>')
34
+ end
35
+ end
36
+
37
+ context 'specifying regexp with text/html response' do
38
+ let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/whatever' }, '<body>'] } }
39
+ let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text', content_type: /\Atext\//) }
40
+
41
+ it "processes" do
42
+ get '/'
43
+ expect(last_response.body).to eq('<body>after-text')
44
+ end
45
+ end
46
+ end
47
+
48
+ describe TransformContent, 'passes on condition' do
49
+ let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
50
+ let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text', pass_condition: ->(env) { true }) }
51
+
52
+ it 'skips when a passed-in condition returns true' do
53
+ get '/'
54
+ expect(last_response.body).to eq('<body>')
55
+ end
56
+ end
57
+
58
+ describe TransformContent, 'insertion after' do
59
+ let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
60
+ let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text') }
61
+
62
+ it 'adds text after the first occurrence of a search string' do
63
+ get '/'
64
+ expect(last_response.body).to eq('<body>after-text')
65
+ end
66
+ end
67
+
68
+ describe TransformContent, 'insertion before' do
69
+ let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
70
+ let(:app) { TransformContent.new(inner_app, regexp: /<body>/, before: 'before-text') }
71
+
72
+ it 'adds text before the first occurrence of a search string' do
73
+ get '/'
74
+ expect(last_response.body).to eq('before-text<body>')
75
+ end
76
+ end
77
+
78
+ describe TransformContent, 'insertion before and after' do
79
+ let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
80
+ let(:app) { TransformContent.new(inner_app, regexp: /<body>/, before: 'before-text', after: 'after-text') }
81
+
82
+ it 'adds text before and after the first occurrence of a search string' do
83
+ get '/'
84
+ expect(last_response.body).to eq('before-text<body>after-text')
85
+ end
86
+ end
87
+
88
+ describe TransformContent, 'insertion before and after with captures' do
89
+ let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<div>foo</div>'] } }
90
+ let(:app) { TransformContent.new(inner_app, regexp: /(<div>)(.*?)(<\/div>)/, before: 'before-text', after: 'after-text') }
91
+
92
+ it 'adds text before and after the middle capture' do
93
+ get '/'
94
+ expect(last_response.body).to eq('<div>before-textfooafter-text</div>')
95
+ end
96
+ end
97
+
98
+ describe TransformContent, 'not 0 or 3 captures' do
99
+ let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<div>foo</div>'] } }
100
+ let(:app) { TransformContent.new(inner_app, regexp: /(<div>).*?(<\/div>)/, before: 'before-text', after: 'after-text') }
101
+
102
+ it 'raises an exception' do
103
+ expect { get '/' }.to raise_error
104
+ end
105
+ end
106
+
107
+ describe TransformContent, 'insertion after with two potential matches' do
108
+ let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body> <body>'] } }
109
+ let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text') }
110
+
111
+ it 'adds text only after the first occurrence of a search string' do
112
+ get '/'
113
+ expect(last_response.body).to eq('<body>after-text <body>')
114
+ end
115
+ end
116
+
117
+ describe TransformContent, 'insertion if response is an Array' do
118
+ let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, [ '<bo', 'dy>' ] ] } }
119
+ let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text') }
120
+
121
+ it 'adds text after the first occurrence of a search string' do
122
+ get '/'
123
+ expect(last_response.body).to eq('<body>after-text')
124
+ end
125
+ end
126
+
127
+ describe TransformContent, 'status code implies no body' do
128
+ let(:inner_app) { ->(env) { [204, { 'Content-Type' => 'text/html' }, '<body>' ] } }
129
+ let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text') }
130
+
131
+ it 'skips' do
132
+ get '/'
133
+ expect(last_response.body).to eq('<body>')
134
+ end
135
+ end
136
+
137
+ describe TransformContent, 'response is transfer-encoded' do
138
+ let(:inner_app) { ->(env) { [200, { 'Transfer-Encoding' => 'Chunked', 'Content-Type' => 'text/html' }, '<body>' ] } }
139
+ let(:app) { TransformContent.new(inner_app, regexp: /<body>/, after: 'after-text') }
140
+
141
+ it 'skips' do
142
+ get '/'
143
+ expect(last_response.body).to eq('<body>')
144
+ end
145
+ end
146
+ end
data/spec/repo.rb ADDED
@@ -0,0 +1,18 @@
1
+ shared_context 'repo' do
2
+ before do
3
+ repo = Grit::Repo.init('repo')
4
+ File.open('repo/.git/config', 'a+') do |f|
5
+ f.write <<-GITCONFIG.strip_heredoc
6
+ [user]
7
+ name = Mortimer Snerd
8
+ email = snerd@example.com
9
+ GITCONFIG
10
+ end
11
+ File.open('repo/Home.md', 'w') { |f| f.write('### Nice wiki page') }
12
+ repo.add('repo/Home.md')
13
+ end
14
+
15
+ after do
16
+ FileUtils.rm_rf 'repo'
17
+ end
18
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,46 @@
1
1
  require 'rack/test'
2
+ require 'ostruct'
2
3
 
3
4
  RSpec.configure do |conf|
4
5
  conf.include Rack::Test::Methods
5
6
  end
7
+
8
+ class String
9
+ def strip_heredoc
10
+ indent = scan(/^[ \t]*(?=\S)/).min.size || 0
11
+ gsub(/^[ \t]{#{indent}}/, '')
12
+ end
13
+ end
14
+
15
+ require 'warden/github'
16
+ require 'warden/test/helpers'
17
+ require 'warden/github/user'
18
+
19
+ # Follows https://github.com/atmos/sinatra_auth_github/blob/master/lib/sinatra/auth/github/test/test_helper.rb
20
+ module Stoor
21
+ module Test
22
+ module Helper
23
+ include Warden::Test::Helpers
24
+
25
+ class User < Warden::GitHub::User
26
+ attr_accessor :api
27
+ end
28
+
29
+ def make_user(login, emails = 'effie@example.com', override_attributes = {})
30
+ emails = Array(emails)
31
+ attributes = {
32
+ 'login' => login,
33
+ 'email' => emails.first,
34
+ 'name' => "Effie Klinker",
35
+ 'company' => "7fff",
36
+ 'gravatar_id' => 'a'*32,
37
+ 'avatar_url' => 'https://a249.e.akamai.net/assets.github.com/images/gravatars/gravatar-140.png?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png',
38
+ }
39
+ User.new(attributes.merge! override_attributes).tap do |user|
40
+ user.api = OpenStruct.new(:emails => emails)
41
+ end
42
+ end
43
+
44
+ end
45
+ end
46
+ end
data/stoor.gemspec CHANGED
@@ -6,11 +6,12 @@ Gem::Specification.new do |s|
6
6
  s.version = Stoor::VERSION
7
7
  s.date = Time.now.utc.strftime('%Y-%m-%d')
8
8
  s.summary = 'Front-end for Gollum'
9
- s.description = 'Front-end for Gollum'
9
+ s.description = 'Stoor is an app to bring up a local Gollum (wiki software) against a Git repo with bells and whistles like auth.'
10
+ s.license = 'MIT'
10
11
  s.authors = ['John G. Norman']
11
12
  s.email = 'john@7fff.com'
12
13
  s.files = `git ls-files`.split("\n")
13
- s.homepage = 'https://rubygems.org/gems/stoor'
14
+ s.homepage = 'https://github.com/jgn/stoor'
14
15
  s.rdoc_options = ['--charset=UTF-8']
15
16
  s.require_paths = ['lib']
16
17
  s.test_files = `git ls-files spec`.split("\n")
@@ -18,8 +19,10 @@ Gem::Specification.new do |s|
18
19
  s.add_dependency 'gollum', '~> 2.5.0'
19
20
  s.add_dependency 'sinatra_auth_github', '~> 1.0.0'
20
21
  s.add_dependency 'json', '~> 1.8.0'
22
+ s.add_dependency 'grit', '~> 2.5.0'
21
23
  s.executables << 'stoor'
22
24
 
25
+ s.add_development_dependency 'warden-github'
23
26
  s.add_development_dependency 'rack-test'
24
27
  s.add_development_dependency 'rspec'
25
28
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stoor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-09-28 00:00:00.000000000 Z
12
+ date: 2013-10-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thin
@@ -75,6 +75,38 @@ dependencies:
75
75
  - - ~>
76
76
  - !ruby/object:Gem::Version
77
77
  version: 1.8.0
78
+ - !ruby/object:Gem::Dependency
79
+ name: grit
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 2.5.0
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 2.5.0
94
+ - !ruby/object:Gem::Dependency
95
+ name: warden-github
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
78
110
  - !ruby/object:Gem::Dependency
79
111
  name: rack-test
80
112
  requirement: !ruby/object:Gem::Requirement
@@ -107,7 +139,8 @@ dependencies:
107
139
  - - ! '>='
108
140
  - !ruby/object:Gem::Version
109
141
  version: '0'
110
- description: Front-end for Gollum
142
+ description: Stoor is an app to bring up a local Gollum (wiki software) against a
143
+ Git repo with bells and whistles like auth.
111
144
  email: john@7fff.com
112
145
  executables:
113
146
  - stoor
@@ -121,22 +154,25 @@ files:
121
154
  - bin/stoor
122
155
  - config.ru
123
156
  - lib/stoor.rb
124
- - lib/stoor/add_after.rb
125
- - lib/stoor/decorate.rb
126
157
  - lib/stoor/git_config.rb
127
158
  - lib/stoor/github_auth.rb
128
159
  - lib/stoor/logger.rb
160
+ - lib/stoor/transform_content.rb
129
161
  - lib/stoor/version.rb
130
162
  - lib/stoor/views/layout.rb
131
163
  - lib/stoor/views/logout.mustache
132
164
  - lib/stoor/views/logout.rb
133
165
  - log/.gitignore
134
166
  - public/.gitignore
135
- - spec/lib/stoor/add_after_spec.rb
167
+ - spec/lib/stoor/git_config_spec.rb
168
+ - spec/lib/stoor/github_auth_spec.rb
169
+ - spec/lib/stoor/transform_content_spec.rb
170
+ - spec/repo.rb
136
171
  - spec/spec_helper.rb
137
172
  - stoor.gemspec
138
- homepage: https://rubygems.org/gems/stoor
139
- licenses: []
173
+ homepage: https://github.com/jgn/stoor
174
+ licenses:
175
+ - MIT
140
176
  post_install_message:
141
177
  rdoc_options:
142
178
  - --charset=UTF-8
@@ -161,5 +197,8 @@ signing_key:
161
197
  specification_version: 3
162
198
  summary: Front-end for Gollum
163
199
  test_files:
164
- - spec/lib/stoor/add_after_spec.rb
200
+ - spec/lib/stoor/git_config_spec.rb
201
+ - spec/lib/stoor/github_auth_spec.rb
202
+ - spec/lib/stoor/transform_content_spec.rb
203
+ - spec/repo.rb
165
204
  - spec/spec_helper.rb
@@ -1,44 +0,0 @@
1
- require 'rack'
2
-
3
- module Stoor
4
- class AddAfter
5
- include Rack::Utils
6
-
7
- attr_reader :status, :headers
8
- attr_reader :rxp, :string, :content_type
9
-
10
- def initialize(app, rxp, string, content_type = 'text/html')
11
- @app, @rxp, @string, @content_type = app, rxp, string, content_type
12
- end
13
-
14
- def call(env)
15
- @status, @headers, response = @app.call(env)
16
- @headers = HeaderHash.new(@headers)
17
-
18
- if has_body && not_encoded && headers['content-type'] &&
19
- headers['content-type'].index(content_type)
20
-
21
- content = Array(response).join('')
22
-
23
- if content =~ rxp
24
- pre, match, post = $`, $&, $'
25
- new_body = pre + match + string + post
26
- headers['Content-Length'] = new_body.bytesize.to_s
27
- return [status, headers, [new_body]]
28
- end
29
- end
30
-
31
- [status, headers, response]
32
- end
33
-
34
- private
35
-
36
- def has_body
37
- !STATUS_WITH_NO_ENTITY_BODY.include?(status)
38
- end
39
-
40
- def not_encoded
41
- !headers['transfer-encoding']
42
- end
43
- end
44
- end
@@ -1,63 +0,0 @@
1
- module Stoor
2
- class Decorate
3
- include Rack::Utils
4
-
5
- FOOTER_REGEXP = /(<div id="footer">)(.*?)(<\/div>)/im
6
-
7
- def initialize(app); @app = app; end
8
-
9
- def call(env)
10
- @request = Rack::Request.new(env)
11
-
12
- if @request.session['gollum.author'].nil?
13
- @request.logger.info "No 'gollum.author' in session - skipping page decoration."
14
- return @app.call(env)
15
- end
16
-
17
- status, headers, response = @app.call(env)
18
- headers = HeaderHash.new(headers)
19
-
20
- if !STATUS_WITH_NO_ENTITY_BODY.include?(status) &&
21
- !headers['transfer-encoding'] &&
22
- headers['content-type'] &&
23
- headers['content-type'].include?("text/html")
24
-
25
- # TODO: If the response isn't an Array, it's a Rack::File or something, so ignore
26
- if response.respond_to? :inject
27
- content = response.inject("") { |content, part| content << part }
28
- if match_data = content.match(FOOTER_REGEXP)
29
- new_body = "" <<
30
- match_data.pre_match <<
31
- match_data[1] <<
32
- before_existing_footer <<
33
- match_data[2] <<
34
- after_existing_footer <<
35
- match_data[3] <<
36
- match_data.post_match
37
- headers['Content-Length'] = new_body.bytesize.to_s
38
- return [status, headers, [new_body]]
39
- end
40
- end
41
- end
42
-
43
- [status, headers, response]
44
- end
45
-
46
- def before_existing_footer
47
- <<-HTML
48
- <div style="float: left;">
49
- HTML
50
- end
51
-
52
- def after_existing_footer
53
- <<-HTML
54
- </div>
55
- <div style="float: right;">
56
- <p style="text-align: right; font-size: .9em; line-height: 1.6em; color: #999; margin: 0.9em 0;">
57
- Commiting as <b>#{@request.session['gollum.author'][:name]}</b> (#{@request.session['gollum.author'][:email]})#{" | <a href='/logout'>Logout</a>" if ENV['GITHUB_AUTHORIZED']}
58
- </p>
59
- </div>
60
- HTML
61
- end
62
- end
63
- end
@@ -1,99 +0,0 @@
1
- require 'stoor/add_after'
2
- require 'spec_helper'
3
-
4
- module Stoor
5
-
6
- describe AddAfter, 'content-type matching' do
7
-
8
- context 'default with text/plain response' do
9
- let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/plain' }, '<body>'] } }
10
- let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff') }
11
-
12
- it "skips" do
13
- get '/'
14
- expect(last_response.body).to eq('<body>')
15
- end
16
- end
17
-
18
- context 'default with text/html response' do
19
- let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
20
- let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff') }
21
-
22
- it "processes" do
23
- get '/'
24
- expect(last_response.body).to eq('<body>stuff')
25
- end
26
- end
27
-
28
- context 'specifying text/plain with text/html response' do
29
- let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
30
- let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff', 'text/plain') }
31
-
32
- it "skips" do
33
- get '/'
34
- expect(last_response.body).to eq('<body>')
35
- end
36
- end
37
-
38
- context 'specifying regexp with text/html response' do
39
- let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/whatever' }, '<body>'] } }
40
- let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff', /\Atext\//) }
41
-
42
- it "processes" do
43
- get '/'
44
- expect(last_response.body).to eq('<body>stuff')
45
- end
46
- end
47
-
48
- end
49
-
50
- describe AddAfter, 'insertion' do
51
- let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body>'] } }
52
- let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff') }
53
-
54
- it 'adds text after the first occurrence of a search string' do
55
- get '/'
56
- expect(last_response.body).to eq('<body>stuff')
57
- end
58
- end
59
-
60
- describe AddAfter, 'insertion with two potential matches' do
61
- let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, '<body> <body>'] } }
62
- let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff') }
63
-
64
- it 'adds text only after the first occurrence of a search string' do
65
- get '/'
66
- expect(last_response.body).to eq('<body>stuff <body>')
67
- end
68
- end
69
-
70
- describe AddAfter, 'insertion if response is an Array' do
71
- let(:inner_app) { ->(env) { [200, { 'Content-Type' => 'text/html' }, [ '<bo', 'dy>' ] ] } }
72
- let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff') }
73
-
74
- it 'adds text after the first occurrence of a search string' do
75
- get '/'
76
- expect(last_response.body).to eq('<body>stuff')
77
- end
78
- end
79
-
80
- describe AddAfter, 'status code implies no body' do
81
- let(:inner_app) { ->(env) { [204, { 'Content-Type' => 'text/html' }, '<body>' ] } }
82
- let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff') }
83
-
84
- it 'skips' do
85
- get '/'
86
- expect(last_response.body).to eq('<body>')
87
- end
88
- end
89
-
90
- describe AddAfter, 'response is transfer-encoded' do
91
- let(:inner_app) { ->(env) { [200, { 'Transfer-Encoding' => 'Chunked', 'Content-Type' => 'text/html' }, '<body>' ] } }
92
- let(:app) { AddAfter.new(inner_app, /<body>/, 'stuff') }
93
-
94
- it 'skips' do
95
- get '/'
96
- expect(last_response.body).to eq('<body>')
97
- end
98
- end
99
- end