wiki 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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