wiki 0.0.1

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.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +29 -0
  6. data/Rakefile +1 -0
  7. data/lib/wiki.rb +2 -0
  8. data/lib/wiki/ReadMe.md +89 -0
  9. data/lib/wiki/config.ru +2 -0
  10. data/lib/wiki/favicon.rb +31 -0
  11. data/lib/wiki/page.rb +74 -0
  12. data/lib/wiki/random_id.rb +5 -0
  13. data/lib/wiki/server.rb +336 -0
  14. data/lib/wiki/server_helpers.rb +66 -0
  15. data/lib/wiki/stores/ReadMe.md +26 -0
  16. data/lib/wiki/stores/all.rb +3 -0
  17. data/lib/wiki/stores/couch.rb +121 -0
  18. data/lib/wiki/stores/file.rb +53 -0
  19. data/lib/wiki/stores/store.rb +38 -0
  20. data/lib/wiki/version.rb +3 -0
  21. data/lib/wiki/views/client/Gruntfile.js +50 -0
  22. data/lib/wiki/views/client/ReadMe.md +67 -0
  23. data/lib/wiki/views/client/build-test.bat +10 -0
  24. data/lib/wiki/views/client/build.bat +8 -0
  25. data/lib/wiki/views/client/builder.pl +41 -0
  26. data/lib/wiki/views/client/client.coffee +3 -0
  27. data/lib/wiki/views/client/client.js +3607 -0
  28. data/lib/wiki/views/client/crosses.png +0 -0
  29. data/lib/wiki/views/client/images/external-link-ltr-icon.png +0 -0
  30. data/lib/wiki/views/client/images/noise.png +0 -0
  31. data/lib/wiki/views/client/images/oops.jpg +0 -0
  32. data/lib/wiki/views/client/js/d3/d3.behavior.js +198 -0
  33. data/lib/wiki/views/client/js/d3/d3.chart.js +984 -0
  34. data/lib/wiki/views/client/js/d3/d3.csv.js +92 -0
  35. data/lib/wiki/views/client/js/d3/d3.geo.js +566 -0
  36. data/lib/wiki/views/client/js/d3/d3.geom.js +825 -0
  37. data/lib/wiki/views/client/js/d3/d3.js +3597 -0
  38. data/lib/wiki/views/client/js/d3/d3.layout.js +1923 -0
  39. data/lib/wiki/views/client/js/d3/d3.time.js +660 -0
  40. data/lib/wiki/views/client/js/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  41. data/lib/wiki/views/client/js/images/ui-icons_222222_256x240.png +0 -0
  42. data/lib/wiki/views/client/js/jquery-1.6.2.min.js +18 -0
  43. data/lib/wiki/views/client/js/jquery-1.7.1.min.js +4 -0
  44. data/lib/wiki/views/client/js/jquery-1.9.1.min.js +5 -0
  45. data/lib/wiki/views/client/js/jquery-migrate-1.1.1.min.js +3 -0
  46. data/lib/wiki/views/client/js/jquery-ui-1.10.1.custom.min.css +5 -0
  47. data/lib/wiki/views/client/js/jquery-ui-1.10.1.custom.min.js +6 -0
  48. data/lib/wiki/views/client/js/jquery-ui-1.8.16.custom.css +339 -0
  49. data/lib/wiki/views/client/js/jquery-ui-1.8.16.custom.min.js +315 -0
  50. data/lib/wiki/views/client/js/jquery.ie.cors.js +310 -0
  51. data/lib/wiki/views/client/js/jquery.ui.touch-punch.min.js +11 -0
  52. data/lib/wiki/views/client/js/modernizr.custom.63710.js +824 -0
  53. data/lib/wiki/views/client/js/sockjs-0.3.min.js +27 -0
  54. data/lib/wiki/views/client/js/underscore-min.js +30 -0
  55. data/lib/wiki/views/client/mkplugin.sh +97 -0
  56. data/lib/wiki/views/client/package.json +36 -0
  57. data/lib/wiki/views/client/runtests.html +26 -0
  58. data/lib/wiki/views/client/style.css +339 -0
  59. data/lib/wiki/views/client/test/mocha.css +231 -0
  60. data/lib/wiki/views/client/test/mocha.js +5340 -0
  61. data/lib/wiki/views/client/test/testclient.js +17133 -0
  62. data/lib/wiki/views/client/testclient.coffee +18 -0
  63. data/lib/wiki/views/client/theme/granite.css +59 -0
  64. data/lib/wiki/views/client/theme/stoneSeamless.jpg +0 -0
  65. data/lib/wiki/views/client/twitter-maintainance.jpg +0 -0
  66. data/lib/wiki/views/layout.haml +56 -0
  67. data/lib/wiki/views/oops.haml +5 -0
  68. data/lib/wiki/views/page.haml +20 -0
  69. data/lib/wiki/views/static.html +30 -0
  70. data/lib/wiki/views/view.haml +2 -0
  71. data/wiki.gemspec +28 -0
  72. metadata +121 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: af6c3920f6679b9844dd249d0c5cc3fd61982799
4
+ data.tar.gz: 4d5247d3d0876a2ae044742b217e74b796e87f96
5
+ SHA512:
6
+ metadata.gz: 8c818d43b7dfeb8e1d2cbcb720fd00a44b7df4784fb82aafd695fef6ffbafa4098d386d7ae872b6a1a76e83bd67afa22de721345455344cec6175848cbf61474
7
+ data.tar.gz: afffe40b5c4488773fa46abf1a602752b49e976e1f1b7d878a96e1935d5623c6cff612dc86ed8753721c24a3bc0cb68cd76ded290642c6fd9799f5bf89cedd14
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in wiki.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Daniel Stark
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Wiki
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'wiki'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install wiki
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,2 @@
1
+ require "wiki/version"
2
+ require "wiki/server"
@@ -0,0 +1,89 @@
1
+ Server Goals
2
+ ============
3
+
4
+ The server participates in a peer-to-peer exchange of page content and page metadata.
5
+ It is expected to be mostly-on so that it can support the needs of peers and anticipate the needs of ux clients of this server.
6
+ In summary, the server's peer-to-peer side exists to:
7
+
8
+ * Encourage the deployment of independently owned content stores.
9
+ * Support community among owners through systematic sharing of content.
10
+
11
+
12
+ Customizing your Server
13
+ =======================
14
+
15
+ The distribution contains default files. They will be copied the first time
16
+ they're requested, if you don't install your own. These are:
17
+
18
+ default-data/pages/welcome-visitors
19
+ default-data/status/favicon.png (unused, see below)
20
+
21
+ The first is the usual welcome page offered as the server's home page.
22
+ The second is a 32x32 png gradient that is used to identify your server
23
+ in bookmarks, browser tabs, page headings and journal entries.
24
+
25
+ You can revise the welcome page by editing your copy here:
26
+
27
+ data/pages/welcome-visitors
28
+
29
+ A suitable random gradient will be generated for you.
30
+ You can remove or replace it here:
31
+
32
+ data/status/favicon.png
33
+
34
+
35
+ Launching the Server
36
+ ====================
37
+
38
+ We're now using Ruby 1.9.2 which we manage with rvm. Launch the server with the following bundler commands:
39
+
40
+ rvm 1.9.2
41
+ bundle exec rackup -s thin -p 1111
42
+
43
+ Hosting a Server Farm
44
+ =====================
45
+
46
+ The server can host separate pages and status directories for a number of virtual hosts. Enable this by creating the subdirectory:
47
+
48
+ data/farm
49
+
50
+ or by setting the environment variable
51
+
52
+ FARM_MODE=true
53
+
54
+ The server will create subdirectories with farm for each virtual host name and locate pages and status directories within that.
55
+
56
+ Recursive Server Calls
57
+ ======================
58
+
59
+ Federated sites hosted in the same farm can cause recursive web requests.
60
+ This is an issue for certain rack servers, notably thin, which is widely used in production rack setups.
61
+ If you have a standard server configuration, in which all traffic coming to *.my-sfw-farm.org will be handled by
62
+ a single server, you can serve page json and favicons in the context of the current request
63
+ (instead of generating an additional HTTP request)
64
+ by setting the environment variable:
65
+
66
+ FARM_DOMAINS=my-sfw-farm.org
67
+
68
+ Your server can handle multiple domains by comma-separating them:
69
+
70
+ FARM_DOMAINS=my-sfw-farm.org,fedwiki.jacksmith.com
71
+
72
+ With this setup, pages and favicons will be served more efficiently, as well as being friendly to single-threaded servers like thin.
73
+
74
+ Alternately, you can use webrick, which handles recursive calls out of the box. Launch it with this command:
75
+
76
+ bundle exec rackup -s webrick -p 1111
77
+
78
+ CouchDB
79
+ =======
80
+
81
+ By default, all pages, favicons, and server claims are stored in the server's local filesystem.
82
+ If you prefer to use CouchDB for storage, set two environment variables:
83
+
84
+ STORE_TYPE=CouchStore
85
+ COUCHDB_URL=https://username:password@some-couchdb-host.com
86
+
87
+ If you want to run a farm with CouchDB, be sure to set the environment variable
88
+
89
+ FARM_MODE=true
@@ -0,0 +1,2 @@
1
+ require File.expand_path('../server', __FILE__)
2
+ run Controller
@@ -0,0 +1,31 @@
1
+ require 'rubygems'
2
+ require 'png'
3
+
4
+ class Favicon
5
+ class << self
6
+ def create_blob
7
+ canvas = PNG::Canvas.new 32, 32
8
+ light = PNG::Color.from_hsv(256*rand,200,255).rgb()
9
+ dark = PNG::Color.from_hsv(256*rand,200,125).rgb()
10
+ angle = 2 * (rand()-0.5)
11
+ sin = Math.sin angle
12
+ cos = Math.cos angle
13
+ scale = sin.abs + cos.abs
14
+ for x in (0..31)
15
+ for y in (0..31)
16
+ p = (sin >= 0 ? sin*x+cos*y : -sin*(31-x)+cos*y) / 31 / scale
17
+ canvas[x,y] = PNG::Color.new(
18
+ light[0]*p + dark[0]*(1-p),
19
+ light[1]*p + dark[1]*(1-p),
20
+ light[2]*p + dark[2]*(1-p))
21
+ end
22
+ end
23
+ PNG.new(canvas).to_blob
24
+ end
25
+
26
+ def get_or_create(path)
27
+ Store.get_blob(path) || Store.put_blob(path, Favicon.create_blob)
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,74 @@
1
+ require 'json'
2
+ require File.expand_path("../random_id", __FILE__)
3
+ require File.expand_path("../stores/all", __FILE__)
4
+
5
+ class PageError < StandardError; end;
6
+
7
+ # Page Class
8
+ # Handles writing and reading JSON data to and from files.
9
+ class Page
10
+
11
+ # Directory where pages are to be stored.
12
+ attr_accessor :directory
13
+ # Directory where default (pre-existing) pages are stored.
14
+ attr_accessor :default_directory
15
+ # Directory where plugins that may have pages are stored.
16
+ attr_accessor :plugins_directory
17
+
18
+ def plugin_page_path name
19
+ Dir.glob(File.join(plugins_directory, '*/pages')) do |dir|
20
+ probe = "#{dir}/#{name}"
21
+ return probe if File.exists? probe
22
+ end
23
+ return nil
24
+ end
25
+
26
+
27
+ # Get a page
28
+ #
29
+ # @param [String] name - The name of the file to retrieve, relative to Page.directory.
30
+ # @return [Hash] The contents of the retrieved page (parsed JSON).
31
+ def get(name)
32
+ assert_attributes_set
33
+ path = File.join(directory, name)
34
+ default_path = File.join(default_directory, name)
35
+ page = Store.get_page(path)
36
+ if page
37
+ page
38
+ elsif File.exist?(default_path)
39
+ FileStore.get_page(default_path)
40
+ elsif (path = plugin_page_path name)
41
+ page = FileStore.get_page(path)
42
+ page['plugin'] = path.match(/plugins\/(.*?)\/pages/)[1]
43
+ page
44
+ else
45
+ halt 404
46
+ end
47
+ end
48
+
49
+ def exists?(name)
50
+ Store.exists?(File.join(directory, name)) or
51
+ File.exist?(File.join(default_directory, name)) or
52
+ !plugin_page_path(name).nil?
53
+ end
54
+
55
+ # Create or update a page
56
+ #
57
+ # @param [String] name - The name of the file to create/update, relative to Page.directory.
58
+ # @param [Hash] page - The page data to be written to the file (it will be converted to JSON).
59
+ # @return [Hash] The contents of the retrieved page (parsed JSON).
60
+ def put(name, page)
61
+ assert_attributes_set
62
+ path = File.join directory, name
63
+ page.delete 'plugin'
64
+ Store.put_page(path, page, :name => name, :directory => directory)
65
+ end
66
+
67
+ private
68
+
69
+ def assert_attributes_set
70
+ raise PageError.new('Page.directory must be set') unless directory
71
+ raise PageError.new('Page.default_directory must be set') unless default_directory
72
+ raise PageError.new('Page.plugins_directory must be set') unless plugins_directory
73
+ end
74
+ end
@@ -0,0 +1,5 @@
1
+ module RandomId
2
+ def self.generate
3
+ (0..15).collect{(rand*16).to_i.to_s(16)}.join
4
+ end
5
+ end
@@ -0,0 +1,336 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ require 'pathname'
4
+ Bundler.require
5
+
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ SINATRA_ROOT = File.expand_path(File.dirname(__FILE__))
8
+ APP_ROOT = File.expand_path(File.join(SINATRA_ROOT, "..", ".."))
9
+
10
+ Encoding.default_external = Encoding::UTF_8
11
+
12
+ require 'server_helpers'
13
+ require 'stores/all'
14
+ require 'random_id'
15
+ require 'page'
16
+ require 'favicon'
17
+
18
+ require 'openid'
19
+ require 'openid/store/filesystem'
20
+
21
+ class Controller < Sinatra::Base
22
+ set :port, 1111
23
+ set :public, File.join(APP_ROOT, "client")
24
+ set :views , File.join(SINATRA_ROOT, "views")
25
+ set :haml, :format => :html5
26
+ set :versions, `git log -10 --oneline` || "no git log"
27
+ if ENV.include?('SESSION_STORE')
28
+ use ENV['SESSION_STORE'].split('::').inject(Object) { |mod, const| mod.const_get(const) }
29
+ else
30
+ enable :sessions
31
+ end
32
+ helpers ServerHelpers
33
+
34
+ Store.set ENV['STORE_TYPE'], APP_ROOT
35
+
36
+ class << self # overridden in test
37
+ def data_root
38
+ File.join APP_ROOT, "data"
39
+ end
40
+ end
41
+
42
+ def farm_page(site=request.host)
43
+ page = Page.new
44
+ page.directory = File.join data_dir(site), "pages"
45
+ page.default_directory = File.join APP_ROOT, "default-data", "pages"
46
+ page.plugins_directory = File.join APP_ROOT, "client", "plugins"
47
+ Store.mkdir page.directory
48
+ page
49
+ end
50
+
51
+ def farm_status(site=request.host)
52
+ status = File.join data_dir(site), "status"
53
+ Store.mkdir status
54
+ status
55
+ end
56
+
57
+ def data_dir(site)
58
+ Store.farm?(self.class.data_root) ? File.join(self.class.data_root, "farm", site) : self.class.data_root
59
+ end
60
+
61
+ def identity
62
+ default_path = File.join APP_ROOT, "default-data", "status", "local-identity"
63
+ real_path = File.join farm_status, "local-identity"
64
+ id_data = Store.get_hash real_path
65
+ id_data ||= Store.put_hash(real_path, FileStore.get_hash(default_path))
66
+ end
67
+
68
+ post "/logout" do
69
+ session.delete :authenticated
70
+ redirect "/"
71
+ end
72
+
73
+ post '/login' do
74
+ begin
75
+ root_url = request.url.match(/(^.*\/{2}[^\/]*)/)[1]
76
+ identifier_file = File.join farm_status, "open_id.identifier"
77
+ identifier = Store.get_text(identifier_file)
78
+ unless identifier
79
+ identifier = params[:identifier]
80
+ end
81
+ open_id_request = openid_consumer.begin(identifier)
82
+
83
+ redirect open_id_request.redirect_url(root_url, root_url + "/login/openid/complete")
84
+ rescue
85
+ oops 400, "Trouble starting OpenID<br>Did you enter a proper endpoint?"
86
+ end
87
+ end
88
+
89
+ get '/login/openid/complete' do
90
+ begin
91
+ response = openid_consumer.complete(params, request.url)
92
+ case response.status
93
+ when OpenID::Consumer::FAILURE
94
+ oops 401, "Login failure"
95
+ when OpenID::Consumer::SETUP_NEEDED
96
+ oops 400, "Setup needed"
97
+ when OpenID::Consumer::CANCEL
98
+ oops 400, "Login cancelled"
99
+ when OpenID::Consumer::SUCCESS
100
+ id = params['openid.identity']
101
+ id_file = File.join farm_status, "open_id.identity"
102
+ stored_id = Store.get_text(id_file)
103
+ if stored_id
104
+ if stored_id == id
105
+ # login successful
106
+ authenticate!
107
+ else
108
+ oops 403, "This is not your wiki"
109
+ end
110
+ else
111
+ Store.put_text id_file, id
112
+ # claim successful
113
+ authenticate!
114
+ end
115
+ else
116
+ oops 400, "Trouble with OpenID"
117
+ end
118
+ rescue
119
+ oops 400, "Trouble running OpenID<br>Did you enter a proper endpoint?"
120
+ end
121
+ end
122
+
123
+ get '/system/slugs.json' do
124
+ content_type 'application/json'
125
+ cross_origin
126
+ JSON.pretty_generate(Dir.entries(farm_page.directory).reject{|e|e[0] == '.'})
127
+ end
128
+
129
+ get '/favicon.png' do
130
+ content_type 'image/png'
131
+ headers 'Cache-Control' => "max-age=3600"
132
+ cross_origin
133
+ Favicon.get_or_create(File.join farm_status, 'favicon.png')
134
+ end
135
+
136
+ get '/random.png' do
137
+ unless authenticated? or (!identified? and !claimed?)
138
+ halt 403
139
+ return
140
+ end
141
+
142
+ content_type 'image/png'
143
+ path = File.join farm_status, 'favicon.png'
144
+ Store.put_blob path, Favicon.create_blob
145
+ end
146
+
147
+ get '/' do
148
+ redirect "/#{identity['root']}.html"
149
+ end
150
+
151
+ get %r{^/data/([\w -]+)$} do |search|
152
+ content_type 'application/json'
153
+ cross_origin
154
+ pages = Store.annotated_pages farm_page.directory
155
+ candidates = pages.select do |page|
156
+ datasets = page['story'].select do |item|
157
+ item['type']=='data' && item['text'] && item['text'].index(search)
158
+ end
159
+ datasets.length > 0
160
+ end
161
+ halt 404 unless candidates.length > 0
162
+ JSON.pretty_generate(candidates.first)
163
+ end
164
+
165
+ get %r{^/([a-z0-9-]+)\.html$} do |name|
166
+ halt 404 unless farm_page.exists?(name)
167
+ haml :page, :locals => { :page => farm_page.get(name), :page_name => name }
168
+ end
169
+
170
+ get %r{^((/[a-zA-Z0-9:.-]+/[a-z0-9-]+(_rev\d+)?)+)$} do
171
+ elements = params[:captures].first.split('/')
172
+ pages = []
173
+ elements.shift
174
+ while (site = elements.shift) && (id = elements.shift)
175
+ if site == 'view'
176
+ pages << {:id => id}
177
+ else
178
+ pages << {:id => id, :site => site}
179
+ end
180
+ end
181
+ haml :view, :locals => {:pages => pages}
182
+ end
183
+
184
+ get '/system/plugins.json' do
185
+ content_type 'application/json'
186
+ cross_origin
187
+ plugins = []
188
+ path = File.join(APP_ROOT, "client/plugins")
189
+ pathname = Pathname.new path
190
+ Dir.glob("#{path}/*/") {|filename| plugins << Pathname.new(filename).relative_path_from(pathname)}
191
+ JSON.pretty_generate plugins
192
+ end
193
+
194
+ get '/system/sitemap.json' do
195
+ content_type 'application/json'
196
+ cross_origin
197
+ pages = Store.annotated_pages farm_page.directory
198
+ sitemap = pages.collect {|p| {"slug" => p['name'], "title" => p['title'], "date" => p['updated_at'].to_i*1000, "synopsis" => synopsis(p)}}
199
+ JSON.pretty_generate sitemap
200
+ end
201
+
202
+ get '/system/factories.json' do
203
+ content_type 'application/json'
204
+ cross_origin
205
+ # return "[]"
206
+ factories = Dir.glob(File.join(APP_ROOT, "client/plugins/*/factory.json")).collect do |info|
207
+ begin
208
+ JSON.parse(File.read(info))
209
+ rescue
210
+ end
211
+ end.reject {|info| info.nil?}
212
+ JSON.pretty_generate factories
213
+ end
214
+
215
+ get %r{^/([a-z0-9-]+)\.json$} do |name|
216
+ content_type 'application/json'
217
+ serve_page name
218
+ end
219
+
220
+ error 403 do
221
+ 'Access forbidden'
222
+ end
223
+
224
+ put %r{^/page/([a-z0-9-]+)/action$} do |name|
225
+ unless authenticated? or (!identified? and !claimed?)
226
+ halt 403
227
+ return
228
+ end
229
+
230
+ action = JSON.parse params['action']
231
+ if site = action['fork']
232
+ # this fork is bundled with some other action
233
+ page = JSON.parse RestClient.get("#{site}/#{name}.json")
234
+ ( page['journal'] ||= [] ) << { 'type' => 'fork', 'site' => site }
235
+ farm_page.put name, page
236
+ action.delete 'fork'
237
+ elsif action['type'] == 'create'
238
+ return halt 409 if farm_page.exists?(name)
239
+ page = action['item'].clone
240
+ elsif action['type'] == 'fork'
241
+ if action['item']
242
+ page = action['item'].clone
243
+ action.delete 'item'
244
+ else
245
+ page = JSON.parse RestClient.get("#{action['site']}/#{name}.json")
246
+ end
247
+ else
248
+ page = farm_page.get(name)
249
+ end
250
+
251
+ case action['type']
252
+ when 'move'
253
+ page['story'] = action['order'].collect{ |id| page['story'].detect{ |item| item['id'] == id } || raise('Ignoring move. Try reload.') }
254
+ when 'add'
255
+ before = action['after'] ? 1+page['story'].index{|item| item['id'] == action['after']} : 0
256
+ page['story'].insert before, action['item']
257
+ when 'remove'
258
+ page['story'].delete_at page['story'].index{ |item| item['id'] == action['id'] }
259
+ when 'edit'
260
+ page['story'][page['story'].index{ |item| item['id'] == action['id'] }] = action['item']
261
+ when 'create', 'fork'
262
+ page['story'] ||= []
263
+ else
264
+ puts "unfamiliar action: #{action.inspect}"
265
+ status 501
266
+ return "unfamiliar action"
267
+ end
268
+ ( page['journal'] ||= [] ) << action
269
+ farm_page.put name, page
270
+ "ok"
271
+ end
272
+
273
+ get %r{^/remote/([a-zA-Z0-9:\.-]+)/([a-z0-9-]+)\.json$} do |site, name|
274
+ content_type 'application/json'
275
+ host = site.split(':').first
276
+ if serve_resources_locally?(host)
277
+ serve_page(name, host)
278
+ else
279
+ RestClient.get "#{site}/#{name}.json" do |response, request, result, &block|
280
+ case response.code
281
+ when 200
282
+ response
283
+ when 404
284
+ halt 404
285
+ else
286
+ response.return!(request, result, &block)
287
+ end
288
+ end
289
+ end
290
+ end
291
+
292
+ get %r{^/remote/([a-zA-Z0-9:\.-]+)/favicon.png$} do |site|
293
+ content_type 'image/png'
294
+ host = site.split(':').first
295
+ if serve_resources_locally?(host)
296
+ Favicon.get_or_create(File.join farm_status(host), 'favicon.png')
297
+ else
298
+ RestClient.get "#{site}/favicon.png"
299
+ end
300
+ end
301
+
302
+ not_found do
303
+ oops 404, "Page not found"
304
+ end
305
+
306
+ put '/submit' do
307
+ content_type 'application/json'
308
+ bundle = JSON.parse params['bundle']
309
+ spawn = "#{(rand*1000000).to_i}.#{request.host}"
310
+ site = request.port == 80 ? spawn : "#{spawn}:#{request.port}"
311
+ bundle.each do |slug, page|
312
+ farm_page(spawn).put slug, page
313
+ end
314
+ citation = {
315
+ "type"=> "reference",
316
+ "id"=> RandomId.generate,
317
+ "site"=> site,
318
+ "slug"=> "recent-changes",
319
+ "title"=> "Recent Changes",
320
+ "text"=> bundle.collect{|slug, page| "<li> [[#{page['title']||slug}]]"}.join("\n")
321
+ }
322
+ action = {
323
+ "type"=> "add",
324
+ "id"=> citation['id'],
325
+ "date"=> Time.new.to_i*1000,
326
+ "item"=> citation
327
+ }
328
+ slug = 'recent-submissions'
329
+ page = farm_page.get slug
330
+ (page['story']||=[]) << citation
331
+ (page['journal']||=[]) << action
332
+ farm_page.put slug, page
333
+ JSON.pretty_generate citation
334
+ end
335
+
336
+ end