/)
21 | end
22 |
23 | it "has the latest commit in the head" do
24 | versions.each do |version|
25 | @body.should match(version)
26 | end
27 | end
28 | end
29 |
30 | describe "GET /" do
31 | before(:all) do
32 | get "/"
33 | @response = last_response
34 | @body = last_response.body
35 | end
36 |
37 | it_behaves_like 'Welcome as HTML'
38 | end
39 |
40 | describe "GET /welcome-visitors.html" do
41 | before(:all) do
42 | get "/welcome-visitors.html"
43 | @response = last_response
44 | @body = last_response.body
45 | end
46 |
47 | it_behaves_like 'Welcome as HTML'
48 | end
49 |
50 | describe "GET /view/welcome-visitors" do
51 | before(:all) do
52 | get "/view/welcome-visitors"
53 | @response = last_response
54 | @body = last_response.body
55 | end
56 |
57 | it_behaves_like 'Welcome as HTML'
58 | end
59 |
60 | describe "GET /view/welcome-visitors/view/indie-web-camp" do
61 | before(:all) do
62 | get "/view/welcome-visitors/view/indie-web-camp"
63 | @response = last_response
64 | @body = last_response.body
65 | end
66 |
67 | it_behaves_like 'Welcome as HTML'
68 |
69 | it "has a div with class 'page' and id 'indie-web-camp'" do
70 | @body.should match(/
/)
71 | end
72 | end
73 |
74 | shared_examples_for "GET to JSON resource" do
75 | it "returns 200" do
76 | @response.status.should == 200
77 | end
78 |
79 | it "returns Content-Type application/json" do
80 | last_response.header["Content-Type"].should == "application/json"
81 | end
82 |
83 | it "returns valid JSON" do
84 | expect {
85 | JSON.parse(@body)
86 | }.should_not raise_error
87 | end
88 | end
89 |
90 | describe "GET /welcome-visitors.json" do
91 | before(:all) do
92 | get "/welcome-visitors.json"
93 | @response = last_response
94 | @body = last_response.body
95 | end
96 |
97 | it_behaves_like "GET to JSON resource"
98 |
99 | context "JSON from GET /welcome-visitors.json" do
100 | before(:all) do
101 | @json = JSON.parse(@body)
102 | end
103 |
104 | it "has a title string" do
105 | @json['title'].class.should == String
106 | end
107 |
108 | it "has a story array" do
109 | @json['story'].class.should == Array
110 | end
111 |
112 | it "has paragraph as first item in story" do
113 | @json['story'].first['type'].should == 'paragraph'
114 | end
115 |
116 | it "has paragraph with text string" do
117 | @json['story'].first['text'].class.should == String
118 | end
119 | end
120 | end
121 |
122 | describe "GET /recent-changes.json" do
123 | def create_sample_pages
124 | page = {
125 | "title" => "A Page",
126 | "story" => [ { "type" => "paragraph", "text" => "Hello test" } ],
127 | "journal" => [ { "type" => "add", "date" => Time.now - 10000 } ]
128 | }
129 |
130 | page_without_journal = {
131 | "title" => "No Journal Here",
132 | "story" => [ { "type" => "paragraph", "text" => "Hello test" } ],
133 | }
134 |
135 | page_without_date_in_journal = {
136 | "title" => "Old journal",
137 | "story" => [ { "type" => "paragraph", "text" => "Hello test" } ],
138 | "journal" => [ {"type" => "add"} ]
139 | }
140 |
141 | pages = {
142 | "a-page" => page,
143 | "page-without-journal" => page_without_journal,
144 | "page-without-date-in-journal" => page_without_date_in_journal
145 | }
146 |
147 | # ====
148 |
149 | pages_path = File.join TestDirs::TEST_DATA_DIR, 'pages'
150 | FileUtils.rm_f pages_path
151 | FileUtils.mkdir_p pages_path
152 |
153 | pages.each do |name, content|
154 | page_path = File.join(pages_path, name)
155 | File.open(page_path, 'w'){|file| file.write(content.to_json)}
156 | end
157 |
158 | end
159 |
160 | before(:all) do
161 | create_sample_pages
162 | get "/recent-changes.json"
163 | @response = last_response
164 | @body = last_response.body
165 | @json = JSON.parse(@body)
166 | end
167 |
168 | it_behaves_like "GET to JSON resource"
169 |
170 | context "the JSON" do
171 | it "has a title string" do
172 | @json['title'].class.should == String
173 | end
174 |
175 | it "has a story array" do
176 | @json['story'].class.should == Array
177 | end
178 |
179 | it "has the heading 'Within a Minute'" do
180 | @json['story'].first['text'].should == "
Within a Minute
"
181 | @json['story'].first['type'].should == 'paragraph'
182 | end
183 |
184 | it "has a listing of the single recent change" do
185 | @json['story'][1]['slug'].should == "a-page"
186 | @json['story'][1]['title'].should == "A Page"
187 | @json['story'][1]['type'].should == 'reference'
188 | end
189 |
190 | it "does not show page without journal" do
191 | @json['story'].map {|s| s['slug'] }.should_not include("page-without-journal")
192 | end
193 |
194 | it "does not show page with journal but without date" do
195 | pending
196 | @json['story'].map {|s| s['slug'] }.should_not include("page-without-date-in-journal")
197 | end
198 | end
199 | end
200 |
201 | describe "GET /non-existent-test-page" do
202 | before(:all) do
203 | @non_existent_page = "#{TestDirs::TEST_DATA_DIR}/pages/non-existent-test-page"
204 | `rm -f #{@non_existent_page}`
205 | end
206 |
207 | it "should return 404" do
208 | get "/non-existent-test-page.json"
209 | last_response.status.should == 404
210 | end
211 |
212 | end
213 |
214 | describe "PUT /non-existent-test-page" do
215 | before(:all) do
216 | @non_existent_page = "#{TestDirs::TEST_DATA_DIR}/pages/non-existent-test-page"
217 | `rm -f #{@non_existent_page}`
218 | end
219 |
220 | it "should create page" do
221 | action = {'type' => 'create', 'id' => "123foobar", 'item' => {'title' => 'non-existent-test-page'}}
222 | put "/page/non-existent-test-page/action", :action => action.to_json
223 | last_response.status.should == 200
224 | File.exist?(@non_existent_page).should == true
225 | end
226 | end
227 |
228 | describe "PUT /welcome-visitors" do
229 |
230 | it "should respond with 409" do
231 | action = {'type' => 'create', 'id' => "123foobar", 'item' => {'title' => 'welcome-visitors'}}
232 | put "/page/welcome-visitors/action", :action => action.to_json
233 | last_response.status.should == 409
234 | end
235 |
236 | end
237 |
238 | describe "PUT /foo twice" do
239 | it "should return a 409 when recreating existing page" do
240 | page_file = "#{TestDirs::TEST_DATA_DIR}/pages/foo"
241 | File.exist?(page_file).should == false
242 |
243 | action = {'type' => 'create', 'id' => "123foobar", 'item' => {'title' => 'foo'}}
244 | put "/page/foo/action", :action => action.to_json
245 |
246 | last_response.status.should == 200
247 | File.exist?(page_file).should == true
248 | page_file_contents = File.read(page_file)
249 |
250 | action = {'type' => 'create', 'id' => "123foobar", 'item' => {'title' => 'spam'}}
251 | put "/page/foo/action", :action => action.to_json
252 | last_response.status.should == 409
253 | File.read(page_file).should == page_file_contents
254 | end
255 | end
256 |
--------------------------------------------------------------------------------
/client/style.css:
--------------------------------------------------------------------------------
1 | .error {
2 | color: #bb0000; }
3 |
4 | a {
5 | text-decoration: none; }
6 |
7 | a img {
8 | border: 0; }
9 |
10 | body {
11 | top: 0;
12 | left: 0;
13 | right: 0;
14 | bottom: 0;
15 | position: absolute;
16 | background: #eeeeee url("/crosses.png");
17 | overflow: hidden;
18 | padding: 0;
19 | margin: 0;
20 | font-family: "Helvetica Neue", helvetica, Verdana, Arial, Sans;
21 | line-height: 1.3;
22 | color: #333333; }
23 |
24 | .main {
25 | top: 0;
26 | left: 0;
27 | right: 0;
28 | bottom: 0;
29 | position: absolute;
30 | bottom: 60px;
31 | margin: 0;
32 | width: 10000%; }
33 |
34 | footer {
35 | border-top: 1px solid #3d3c43;
36 | box-shadow: inset 0px 0px 7px rgba(0, 0, 0, 0.8);
37 | background: #eeeeee url("/images/noise.png");
38 | position: fixed;
39 | left: 0;
40 | right: 0;
41 | height: 20px;
42 | padding: 10px;
43 | font-size: 80%;
44 | z-index: 1000;
45 | color: #ccdcd2; }
46 |
47 | footer form {
48 | display: inline;
49 | }
50 |
51 | .neighbor {
52 | float: right;
53 | padding-left: 8px;
54 | width: 16px;
55 | }
56 |
57 | img.remote,
58 | .neighbor img {
59 | width: 16px;
60 | height: 16px;
61 | background-color: #cccccc;
62 | }
63 |
64 | .neighbor .wait {
65 | -webkit-animation: rotatecw 2s linear infinite;
66 | -moz-animation: rotatecw 2s linear infinite;
67 | animation: rotatecw 2s linear infinite;
68 | }
69 | .fetch {
70 | -webkit-animation: rotatecw .5s linear infinite;
71 | -moz-animation: rotatecw .5s linear infinite;
72 | animation: rotatecw .5s linear infinite;
73 | }
74 | .fail {
75 | -webkit-transform: rotate(15deg);
76 | -moz-transform: rotate(15deg);
77 | transform: rotate(15deg);
78 | }
79 | @-webkit-keyframes rotatecw { from { -webkit-transform: rotate(0deg); } to { -webkit-transform: rotate(360deg); } }
80 | @-moz-keyframes rotatecw { from { -webkit-transform: rotate(0deg); } to { -webkit-transform: rotate(360deg); } }
81 | @keyframes rotatecw { from { -webkit-transform: rotate(0deg); } to { -webkit-transform: rotate(360deg); } }
82 |
83 |
84 | header {
85 | top: 0; }
86 |
87 | footer {
88 | bottom: 0; }
89 |
90 | .twins,
91 | .journal,
92 | .footer {
93 | min-height: 1em;
94 | opacity: 1;
95 | }
96 | .twins:hover,
97 | .journal:hover,
98 | .footer:hover {
99 | opacity: 1;
100 | }
101 |
102 | .story {
103 | padding-bottom: 5px; }
104 |
105 | .data,
106 | .chart,
107 | .image {
108 | float: right;
109 | margin-left: 0.4em;
110 | margin-bottom: 0.4em;
111 | background: #eeeeee;
112 | padding: 0.8em;
113 | width: 42%;
114 | }
115 |
116 | .image .thumbnail {
117 | width: 100%; }
118 |
119 | .journal {
120 | width: 420px;
121 | overflow-x: hidden;
122 | margin-top: 2px;
123 | clear: both;
124 | background-color: #eeeeee;
125 | overflow: auto;
126 | padding: 3px; }
127 |
128 | .action.fork {
129 | color: black; }
130 |
131 | .action {
132 | font-size: 0.9em;
133 | background-color: #cccccc;
134 | color: #666666;
135 | text-align: center;
136 | text-decoration: none;
137 | padding: 0.2em;
138 | margin: 3px;
139 | float: left;
140 | width: 18px; }
141 |
142 | .action.separator {
143 | background-color: #eeeeee;
144 | }
145 |
146 | .control-buttons {
147 | float: right;
148 | position: relative;
149 | right: 5px;
150 | overflow-x: visible;
151 | white-space: nowrap; }
152 |
153 | .button {
154 | /*from bootstrap*/
155 | background-color: #f5f5f5;
156 | *background-color: #e6e6e6;
157 | background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6);
158 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));
159 | background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6);
160 | background-image: -o-linear-gradient(top, #ffffff, #e6e6e6);
161 | background-image: linear-gradient(top, #ffffff, #e6e6e6);
162 | background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6);
163 | background-repeat: repeat-x;
164 | border: 1px solid #cccccc;
165 | *border: 0;
166 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
167 | border-color: #e6e6e6 #e6e6e6 #bfbfbf;
168 | border-bottom-color: #b3b3b3;
169 | -webkit-border-radius: 4px;
170 | -moz-border-radius: 4px;
171 | border-radius: 4px;
172 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
173 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
174 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
175 |
176 | /*custom*/
177 | display: inline-block;
178 | white-space: normal;
179 | position: relative;
180 | top: 2px;
181 | left: 3px;
182 | font-size: 0.9em;
183 | text-align: center;
184 | text-decoration: none;
185 | padding: 0.2em;
186 | margin-bottom: 2px;
187 | color: #2c3f39;
188 | width: 18px; }
189 |
190 | .button:hover {
191 | color: #d9a513;
192 | text-shadow: 0 0 1px #2c3f39;
193 | box-shadow: 0 0 7px #3d3c43; }
194 |
195 | .button:active {
196 | box-shadow: inset 0 0 7px #3d3c43; }
197 |
198 | .target {
199 | background-color: #ffffcc !important; }
200 |
201 | .report p,
202 | .factory p,
203 | .data p,
204 | .chart p,
205 | .footer a,
206 | .image p,
207 | p.caption {
208 | text-align: center;
209 | margin-bottom: 0;
210 | color: gray;
211 | font-size: 70%; }
212 |
213 | .twins p {
214 | color: gray;
215 | }
216 |
217 | image.remote {
218 | width:16px;
219 | height:16px;
220 | }
221 |
222 | p.readout {
223 | text-align: center;
224 | font-size: 300%;
225 | color: black;
226 | font-weight: bold;
227 | margin: 0; }
228 |
229 | .footer {
230 | clear: both;
231 | margin-bottom: 1em; }
232 |
233 | .page {
234 | float: left;
235 | margin: 8px;
236 | padding: 0 31px;
237 | width: 430px;
238 | background-color: white;
239 | height: 100%;
240 | overflow: auto;
241 | box-shadow: 2px 1px 4px rgba(0, 0, 0, 0.2); }
242 | .page.active {
243 | box-shadow: 2px 1px 24px rgba(0, 0, 0, 0.4);
244 | z-index: 10; }
245 |
246 | .page.plugin {
247 | box-shadow: inset 0px 0px 40px 0px rgba(0,220,0,.5);
248 | }
249 |
250 | .page.local {
251 | box-shadow: inset 0px 0px 40px 0px rgba(220,180,0,.7);
252 | }
253 |
254 | .page.remote {
255 | box-shadow: inset 0px 0px 40px 0px rgba(0,180,220,.5);
256 | }
257 |
258 | .factory,
259 | textarea {
260 | font-size: inherit;
261 | width: 100%;
262 | height: 150px; }
263 |
264 | .clickable:hover {
265 | cursor: pointer; }
266 |
267 | @media all and (max-width: 400px) {
268 | .page {
269 | width: 0.9%;
270 | margin: 0.01%;
271 | padding: 0.04%; } }
272 | ::-webkit-scrollbar {
273 | width: 10px;
274 | height: 10px;
275 | margin-right: 10px; }
276 |
277 | ::-webkit-scrollbar-button:start:decrement,
278 | ::-webkit-scrollbar-button:end:increment {
279 | height: 30px;
280 | display: block;
281 | background-color: transparent; }
282 |
283 | ::-webkit-scrollbar-track-piece {
284 | background-color: #eeeeee;
285 | -webkit-border-radius: 6px; }
286 |
287 | ::-webkit-scrollbar-thumb:vertical {
288 | height: 50px;
289 | background-color: #cccccc;
290 | border: 1px solid #eeeeee;
291 | -webkit-border-radius: 6px; }
292 |
293 | .factory {
294 | clear: both;
295 | margin-top: 5px;
296 | margin-bottom: 5px;
297 | background-color: #eeeeee; }
298 | .factory p {
299 | padding: 10px; }
300 |
301 | .bytebeat .play {
302 | display: inline-block;
303 | height: 14px;
304 | width: 11px;
305 | padding-left: 3px;
306 | font-size: 70%;
307 | color: #999999;
308 | border: 1px solid #999999;
309 | border-radius: 8px; }
310 | .bytebeat .symbol {
311 | color: #990000; }
312 |
313 | .revision {
314 | position: relative;
315 | font-size: 24px;
316 | color: rgba(128,32,16,0.7); #7b2106
317 | font-weight: bold; }
318 |
319 | .revision span {
320 | background: rgba(255,255,255,0.8);
321 | padding: 10px;
322 | position: absolute;
323 | display: block;
324 | text-align: center;
325 | top: -40px;
326 | right: 10px;
327 | -webkit-transform: rotate(-15deg);
328 | -moz-transform: rotate(-15deg);
329 |
330 | }
331 |
332 | .favicon {
333 | position: relative;
334 | margin-bottom: -6px; }
335 |
336 | .ghost {
337 | opacity: 0.6;
338 | border-color: #eef2fe;
339 | }
340 |
--------------------------------------------------------------------------------
/browser-extensions/Chrome/Wiki/main.js:
--------------------------------------------------------------------------------
1 | //
2 | // main background script module.
3 |
4 | // ignore the inner frames
5 | if (window.top === window) {
6 |
7 | var _tabs = {
8 | getKey: function(tabId) {
9 | return "_" + String(tabId); },
10 | cleanup: function(){
11 | var s = this, m, now = (new Date()).valueOf();
12 | for(var p in s.messages) {
13 | if ((m=s.messages[p])&&(m["expire"]
wiki: Add to your Wiki' }); }
44 | chrome.omnibox.onInputStarted.addListener(function() {
45 | resetDefaultSuggestion(); });
46 | chrome.omnibox.onInputCancelled.addListener(function() {
47 | resetDefaultSuggestion(); });
48 | chrome.omnibox.onInputChanged.addListener(function() {
49 | resetDefaultSuggestion(); });
50 | chrome.omnibox.onInputEntered.addListener(function(text) {
51 | try { chrome.tabs.getSelected(null, execWikiAction(text)); } catch (e) { ; } });
52 | context.resetDefaultSuggestion();
53 | }
54 | return context;
55 | })();
56 |
57 | var isSupportedUrl = function(u) {
58 | return ( u && ( u.startsWith( "http://" ) || u.startsWith( "https://" ) ) &&
59 | ( !u.startsWith( _options["wikiUrl"]() ) ) ); };
60 |
61 | var getWikiTab = function(tabs) {
62 | var wikiUrl = _options["wikiUrl"]();
63 | for (var i = 0; i < tabs.length; i++) {
64 | with (tabs[i]) {
65 | if( url.startsWith(wikiUrl)) {
66 | return tabs[i]; } } }
67 | return null; };
68 |
69 | var requestStoreToWiki = function(tabId, arg) {
70 | chrome.tabs.sendRequest(
71 | tabId, arg,
72 | function(m){
73 | // As it turns, if no one listens to our
74 | // background page request, the callback is not
75 | // getting called at all. Hence he deferred parameter
76 | // and the local message cache.
77 | if (!m) { return; }
78 | } ); };
79 |
80 | var storeToWiki = function(srcTab,dstTab,deferred) {
81 | if (!( srcTab && dstTab ) ) { return; }
82 | var s = srcTab, d = dstTab;
83 | chrome.tabs.sendRequest( s.id, { name: "clip" },
84 | function(m) {
85 | if (!m) { return; }
86 | var arg = { name: "persist", content: m.content };
87 | if (deferred) {
88 | _tabs.putMessage( d.id, arg, function(m1,m2){
89 | return ( m1.content ? String(m1.content) + "\r\n": "" ) +
90 | ( m2.content ? String(m2.content) : "" ); } ); }
91 | else {
92 | requestStoreToWiki( d.id, arg ); } } );
93 | };
94 |
95 | var execWikiAction = function(action) {
96 | return function(argTab) {
97 | // TODO: make sure the activity script is loaded. for now just check the
98 | // protocol and assume the script is available
99 | var u; if ( !isSupportedUrl( argTab.url ) ) {
100 | return; }
101 | // locate the wiki container
102 | chrome.windows.getAll( { populate: true }, function(windows){
103 | var tab;
104 | windows.forEach( function(win) {
105 | if (tab) { return; }
106 | tab = getWikiTab(win.tabs);
107 | });
108 | if (tab) { // just select
109 | chrome.tabs.update(tab.id, { "selected": true });
110 | storeToWiki(argTab, tab); }
111 | else { // or navigate to a new one, but be careful with the context
112 | (function(src){
113 | var s = src;
114 | chrome.tabs.create({ "url": _options["wikiUrl"]() },
115 | function(t){ storeToWiki( s, t, true ); } ); } )( argTab ) };
116 | } );
117 | };
118 | };
119 |
120 | var hasStatus = function(tab, changeInfo, status ) {
121 | return ((tab && (tab.status == status)) ||
122 | (changeInfo && (changeInfo.status == status)));
123 | };
124 |
125 | var getUrl = function(tab, changeInfo ) {
126 | return ( ( tab && tab.url ) ? tab.url :
127 | ( ( changeInfo && changeInfo.url ) ? changeInfo.url: "" ) );
128 | };
129 |
130 | var hookActivity = function(id) {
131 | chrome.tabs.executeScript(id, { file: "runtime.js" });
132 | chrome.tabs.executeScript(id, { file: "activity.js" });
133 | };
134 |
135 | var setBadge = function(enabled) {
136 | chrome.browserAction.setBadgeText({ "text": enabled ? "+": "" });
137 | };
138 |
139 | var handleWindowActivate = function(id) {
140 | if (id===chrome.windows.WINDOW_ID_NONE) {
141 | setBadge( false ); }
142 | else {
143 | chrome.tabs.getSelected( null, function(t){ handleNewTab( null, null, t ) } ); };
144 | };
145 |
146 | var handleNewTab = function(tabId, changeInfo, tab) {
147 | if (!tab) { tab = tabId; if (!tab) { return; } }
148 | if (tab.incognito) { return; }
149 |
150 | debug.print("tab " + tab.id.toString() + " changed: " + dumpObject(changeInfo));
151 |
152 | var u; if (hasStatus(tab, changeInfo, "complete")) {
153 | u = tab.url;
154 | // TODO: ensure content script is loaded. while doind so, silence any errors that may occur
155 | }
156 | // the badge and button state ties to the active window/tab
157 | if (tab.selected) {
158 | setBadge( isSupportedUrl(u) ); }
159 | _tabs.cleanup();
160 | };
161 |
162 | var handleTabClose = function(tabId) {
163 | _tabs.getMessage( tabId ); _tabs.cleanup();
164 | return; };
165 |
166 | // processes requests from the browser tabs
167 | var handleRequest = function(request, sender, response) {
168 | switch( request.name ) {
169 | case "fetch":
170 | var t, m; if ((t=sender)&&(t=t.tab)&&(t=t.id)) {
171 | if (m=_tabs.getMessage(t)) {
172 | response( m ); } }
173 | break; }
174 | _tabs.cleanup();
175 | return; };
176 |
177 | // init the extension
178 | (function() {
179 | with ( chrome.browserAction ) {
180 | onClicked.addListener( execWikiAction("add") );
181 | setBadgeBackgroundColor({ "color": [255, 0, 0, 255] });
182 | };
183 | with (chrome.tabs) {
184 | onRemoved.addListener( handleTabClose );
185 | onUpdated.addListener( handleNewTab );
186 | onCreated.addListener( handleNewTab );
187 | onSelectionChanged.addListener(
188 | function(tabId) {
189 | get(tabId,
190 | function(tab) {
191 | handleNewTab(tab);
192 | });
193 | });
194 | };
195 | with (chrome.windows) {
196 | onFocusChanged.addListener( handleWindowActivate );
197 | };
198 | with (chrome.extension) {
199 | onRequest.addListener(handleRequest);
200 | };
201 |
202 | })();
203 |
204 | }
--------------------------------------------------------------------------------
/server/sinatra/server.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'bundler'
3 | require 'pathname'
4 | require 'pp'
5 | Bundler.require
6 |
7 | $LOAD_PATH.unshift(File.dirname(__FILE__))
8 | SINATRA_ROOT = File.expand_path(File.dirname(__FILE__))
9 | APP_ROOT = File.expand_path(File.join(SINATRA_ROOT, "..", ".."))
10 |
11 | Encoding.default_external = Encoding::UTF_8
12 |
13 | require 'server_helpers'
14 | require 'stores/all'
15 | require 'random_id'
16 | require 'page'
17 | require 'favicon'
18 |
19 | require 'openid'
20 | require 'openid/store/filesystem'
21 |
22 | class Controller < Sinatra::Base
23 | set :port, 1111
24 | set :public, File.join(APP_ROOT, "client")
25 | set :views , File.join(SINATRA_ROOT, "views")
26 | set :haml, :format => :html5
27 | set :versions, `git log -10 --oneline` || "no git log"
28 | if ENV.include?('SESSION_STORE')
29 | use ENV['SESSION_STORE'].split('::').inject(Object) { |mod, const| mod.const_get(const) }
30 | else
31 | enable :sessions
32 | end
33 | helpers ServerHelpers
34 |
35 | Store.set ENV['STORE_TYPE'], APP_ROOT
36 |
37 | class << self # overridden in test
38 | def data_root
39 | File.join APP_ROOT, "data"
40 | end
41 | end
42 |
43 | def farm_page(site=request.host)
44 | page = Page.new
45 | page.directory = File.join data_dir(site), "pages"
46 | page.default_directory = File.join APP_ROOT, "default-data", "pages"
47 | page.plugins_directory = File.join APP_ROOT, "client", "plugins"
48 | Store.mkdir page.directory
49 | page
50 | end
51 |
52 | def farm_status(site=request.host)
53 | status = File.join data_dir(site), "status"
54 | Store.mkdir status
55 | status
56 | end
57 |
58 | def data_dir(site)
59 | Store.farm?(self.class.data_root) ? File.join(self.class.data_root, "farm", site) : self.class.data_root
60 | end
61 |
62 | def identity
63 | default_path = File.join APP_ROOT, "default-data", "status", "local-identity"
64 | real_path = File.join farm_status, "local-identity"
65 | id_data = Store.get_hash real_path
66 | id_data ||= Store.put_hash(real_path, FileStore.get_hash(default_path))
67 | end
68 |
69 | post "/logout" do
70 | session.delete :authenticated
71 | redirect "/"
72 | end
73 |
74 | post '/login' do
75 | begin
76 | root_url = request.url.match(/(^.*\/{2}[^\/]*)/)[1]
77 | identifier_file = File.join farm_status, "open_id.identifier"
78 | identifier = Store.get_text(identifier_file)
79 | unless identifier
80 | identifier = params[:identifier]
81 | end
82 | open_id_request = openid_consumer.begin(identifier)
83 |
84 | redirect open_id_request.redirect_url(root_url, root_url + "/login/openid/complete")
85 | rescue
86 | oops 400, "Trouble starting OpenID
Did you enter a proper endpoint?"
87 | end
88 | end
89 |
90 | get '/login/openid/complete' do
91 | begin
92 | response = openid_consumer.complete(params, request.url)
93 | case response.status
94 | when OpenID::Consumer::FAILURE
95 | oops 401, "Login failure"
96 | when OpenID::Consumer::SETUP_NEEDED
97 | oops 400, "Setup needed"
98 | when OpenID::Consumer::CANCEL
99 | oops 400, "Login cancelled"
100 | when OpenID::Consumer::SUCCESS
101 | id = params['openid.identity']
102 | id_file = File.join farm_status, "open_id.identity"
103 | stored_id = Store.get_text(id_file)
104 | if stored_id
105 | if stored_id == id
106 | # login successful
107 | authenticate!
108 | else
109 | oops 403, "This is not your wiki"
110 | end
111 | else
112 | Store.put_text id_file, id
113 | # claim successful
114 | authenticate!
115 | end
116 | else
117 | oops 400, "Trouble with OpenID"
118 | end
119 | rescue
120 | oops 400, "Trouble running OpenID
Did you enter a proper endpoint?"
121 | end
122 | end
123 |
124 | get '/system/slugs.json' do
125 | content_type 'application/json'
126 | cross_origin
127 | JSON.pretty_generate(Dir.entries(farm_page.directory).reject{|e|e[0] == '.'})
128 | end
129 |
130 | get '/favicon.png' do
131 | content_type 'image/png'
132 | headers 'Cache-Control' => "max-age=3600"
133 | cross_origin
134 | Favicon.get_or_create(File.join farm_status, 'favicon.png')
135 | end
136 |
137 | get '/random.png' do
138 | unless authenticated? or (!identified? and !claimed?)
139 | halt 403
140 | return
141 | end
142 |
143 | content_type 'image/png'
144 | path = File.join farm_status, 'favicon.png'
145 | Store.put_blob path, Favicon.create_blob
146 | end
147 |
148 | get '/' do
149 | redirect "/#{identity['root']}.html"
150 | end
151 |
152 | get %r{^/data/([\w -]+)$} do |search|
153 | content_type 'application/json'
154 | cross_origin
155 | pages = Store.annotated_pages farm_page.directory
156 | candidates = pages.select do |page|
157 | datasets = page['story'].select do |item|
158 | item['type']=='data' && item['text'] && item['text'].index(search)
159 | end
160 | datasets.length > 0
161 | end
162 | halt 404 unless candidates.length > 0
163 | JSON.pretty_generate(candidates.first)
164 | end
165 |
166 | get %r{^/([a-z0-9-]+)\.html$} do |name|
167 | halt 404 unless farm_page.exists?(name)
168 | haml :page, :locals => { :page => farm_page.get(name), :page_name => name }
169 | end
170 |
171 | get %r{^((/[a-zA-Z0-9:.-]+/[a-z0-9-]+(_rev\d+)?)+)$} do
172 | elements = params[:captures].first.split('/')
173 | pages = []
174 | elements.shift
175 | while (site = elements.shift) && (id = elements.shift)
176 | if site == 'view'
177 | pages << {:id => id}
178 | else
179 | pages << {:id => id, :site => site}
180 | end
181 | end
182 | haml :view, :locals => {:pages => pages}
183 | end
184 |
185 | get '/system/plugins.json' do
186 | content_type 'application/json'
187 | cross_origin
188 | plugins = []
189 | path = File.join(APP_ROOT, "client/plugins")
190 | pathname = Pathname.new path
191 | Dir.glob("#{path}/*/") {|filename| plugins << Pathname.new(filename).relative_path_from(pathname)}
192 | JSON.pretty_generate plugins
193 | end
194 |
195 | get '/system/sitemap.json' do
196 | content_type 'application/json'
197 | cross_origin
198 | pages = Store.annotated_pages farm_page.directory
199 | sitemap = pages.collect {|p| {"slug" => p['name'], "title" => p['title'], "date" => p['updated_at'].to_i*1000, "synopsis" => synopsis(p)}}
200 | JSON.pretty_generate sitemap
201 | end
202 |
203 | get '/system/factories.json' do
204 | content_type 'application/json'
205 | cross_origin
206 | # return "[]"
207 | factories = Dir.glob(File.join(APP_ROOT, "client/plugins/*/factory.json")).collect do |info|
208 | begin
209 | JSON.parse(File.read(info))
210 | rescue
211 | end
212 | end.reject {|info| info.nil?}
213 | JSON.pretty_generate factories
214 | end
215 |
216 | get %r{^/([a-z0-9-]+)\.json$} do |name|
217 | content_type 'application/json'
218 | serve_page name
219 | end
220 |
221 | error 403 do
222 | 'Access forbidden'
223 | end
224 |
225 | put %r{^/page/([a-z0-9-]+)/action$} do |name|
226 | unless authenticated? or (!identified? and !claimed?)
227 | halt 403
228 | return
229 | end
230 |
231 | action = JSON.parse params['action']
232 | if site = action['fork']
233 | # this fork is bundled with some other action
234 | page = JSON.parse RestClient.get("#{site}/#{name}.json")
235 | ( page['journal'] ||= [] ) << { 'type' => 'fork', 'site' => site }
236 | farm_page.put name, page
237 | action.delete 'fork'
238 | elsif action['type'] == 'create'
239 | return halt 409 if farm_page.exists?(name)
240 | page = action['item'].clone
241 | elsif action['type'] == 'fork'
242 | if action['item']
243 | page = action['item'].clone
244 | action.delete 'item'
245 | else
246 | page = JSON.parse RestClient.get("#{action['site']}/#{name}.json")
247 | end
248 | else
249 | page = farm_page.get(name)
250 | end
251 |
252 | case action['type']
253 | when 'move'
254 | page['story'] = action['order'].collect{ |id| page['story'].detect{ |item| item['id'] == id } || raise('Ignoring move. Try reload.') }
255 | when 'add'
256 | before = action['after'] ? 1+page['story'].index{|item| item['id'] == action['after']} : 0
257 | page['story'].insert before, action['item']
258 | when 'remove'
259 | page['story'].delete_at page['story'].index{ |item| item['id'] == action['id'] }
260 | when 'edit'
261 | page['story'][page['story'].index{ |item| item['id'] == action['id'] }] = action['item']
262 | when 'create', 'fork'
263 | page['story'] ||= []
264 | else
265 | puts "unfamiliar action: #{action.inspect}"
266 | status 501
267 | return "unfamiliar action"
268 | end
269 | ( page['journal'] ||= [] ) << action
270 | farm_page.put name, page
271 | "ok"
272 | end
273 |
274 | get %r{^/remote/([a-zA-Z0-9:\.-]+)/([a-z0-9-]+)\.json$} do |site, name|
275 | content_type 'application/json'
276 | host = site.split(':').first
277 | if serve_resources_locally?(host)
278 | serve_page(name, host)
279 | else
280 | RestClient.get "#{site}/#{name}.json" do |response, request, result, &block|
281 | case response.code
282 | when 200
283 | response
284 | when 404
285 | halt 404
286 | else
287 | response.return!(request, result, &block)
288 | end
289 | end
290 | end
291 | end
292 |
293 | get %r{^/remote/([a-zA-Z0-9:\.-]+)/favicon.png$} do |site|
294 | content_type 'image/png'
295 | host = site.split(':').first
296 | if serve_resources_locally?(host)
297 | Favicon.get_or_create(File.join farm_status(host), 'favicon.png')
298 | else
299 | RestClient.get "#{site}/favicon.png"
300 | end
301 | end
302 |
303 | not_found do
304 | oops 404, "Page not found"
305 | end
306 |
307 | put '/submit' do
308 | content_type 'application/json'
309 | bundle = JSON.parse params['bundle']
310 | spawn = "#{(rand*1000000).to_i}.#{request.host}"
311 | site = request.port == 80 ? spawn : "#{spawn}:#{request.port}"
312 | bundle.each do |slug, page|
313 | farm_page(spawn).put slug, page
314 | end
315 | citation = {
316 | "type"=> "reference",
317 | "id"=> RandomId.generate,
318 | "site"=> site,
319 | "slug"=> "recent-changes",
320 | "title"=> "Recent Changes",
321 | "text"=> bundle.collect{|slug, page| " [[#{page['title']||slug}]]"}.join("\n")
322 | }
323 | action = {
324 | "type"=> "add",
325 | "id"=> citation['id'],
326 | "date"=> Time.new.to_i*1000,
327 | "item"=> citation
328 | }
329 | slug = 'recent-submissions'
330 | page = farm_page.get slug
331 | (page['story']||=[]) << citation
332 | (page['journal']||=[]) << action
333 | farm_page.put slug, page
334 | JSON.pretty_generate citation
335 | end
336 |
337 | end
338 |
--------------------------------------------------------------------------------
/spec/integration_spec.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/spec_helper'
2 |
3 | require 'pathname'
4 | require 'digest/sha1'
5 | require 'net/http'
6 |
7 |
8 |
9 | describe "loading a page" do
10 |
11 | it "should load the welcome page" do
12 | visit("/")
13 | body.should include("Welcome Visitors")
14 | end
15 |
16 | it "should copy welcome-visitors from the default-data to data" do
17 | File.exist?(File.join(TestDirs::TEST_DATA_DIR, "pages/welcome-visitors")).should == false
18 | visit("/")
19 | body.should include("Welcome Visitors")
20 | File.exist?(File.join(TestDirs::TEST_DATA_DIR, "pages/welcome-visitors")).should == true
21 | end
22 |
23 | it "should load multiple pages at once" do
24 | visit("/view/welcome-visitors/view/multiple-paragraphs")
25 | body.should include("Welcome to the")
26 | end
27 |
28 | it "should load remote page" do
29 | remote = "localhost:#{Capybara.server_port}"
30 | visit("/#{remote}/welcome-visitors")
31 | body.should include("Welcome to the")
32 | end
33 |
34 | it "should load a page from plugins" do
35 | visit("/view/air-temperature")
36 | body.should include("Air Temperature")
37 | end
38 |
39 | end
40 |
41 | class Capybara::Node::Element
42 | def double_click
43 | driver.browser.mouse.double_click(native)
44 | end
45 |
46 | TRIGGER_JS = "$(arguments[0]).trigger(arguments[1]);"
47 | def trigger(event)
48 | driver.browser.execute_script(TRIGGER_JS, native, event)
49 | end
50 |
51 | def drag_down(number)
52 | driver.resynchronize do
53 | driver.browser.execute_script "$(arguments[0]).simulateDragSortable({move: arguments[1]});", native, number
54 | end
55 | end
56 |
57 | def roll_over
58 | trigger "mouseover"
59 | end
60 |
61 | def roll_out
62 | trigger "mouseout"
63 | end
64 | end
65 |
66 | class Capybara::Session
67 | def back
68 | execute_script("window.history.back()")
69 | end
70 |
71 | def load_test_library!
72 | Dir["#{TestDirs::JS_DIR}/*.js"].each do |file|
73 | driver.browser.execute_script File.read(file)
74 | end
75 | end
76 |
77 | AJAX_TIMEOUT_LIMIT = 5
78 | def wait_for_ajax_to_complete!
79 | start = Time.now
80 | while evaluate_script("window.jQuery.active") != 0 do
81 | raise Timeout::Error.new("AJAX request timed out") if Time.now - start > AJAX_TIMEOUT_LIMIT
82 | end
83 | end
84 |
85 | def visit_with_wait_for_ajax(*args)
86 | visit_without_wait_for_ajax(*args)
87 | wait_for_ajax_to_complete!
88 | end
89 | alias_method :visit_without_wait_for_ajax, :visit
90 | alias_method :visit, :visit_with_wait_for_ajax
91 | end
92 |
93 | def pause
94 | STDIN.read(1)
95 | end
96 |
97 | module IntegrationHelpers
98 | def journal
99 | page.find(".journal").all(".action")
100 | end
101 |
102 | def first_paragraph
103 | page.find(".paragraph:first")
104 | end
105 |
106 | end
107 |
108 | describe "edit paragraph in place" do
109 | before do
110 | visit("/")
111 | end
112 | include IntegrationHelpers
113 |
114 |
115 | def double_click_paragraph
116 | first_paragraph.double_click
117 | end
118 |
119 | def text_area
120 | first_paragraph.find("textarea")
121 | end
122 |
123 | def replace_and_save(value)
124 | text_area.set value
125 | text_area.trigger "focusout"
126 | end
127 |
128 | it "should turn into a text area, showing wikitext when double-clicking" do
129 | double_click_paragraph
130 | text_area.value.should include("Welcome to the [[Smallest Federated Wiki]]")
131 | end
132 |
133 | it "should save changes to wiki text when unfocused" do
134 | double_click_paragraph
135 | replace_and_save("The [[quick brown]] fox.")
136 | first_paragraph.text.should include("The quick brown fox")
137 | end
138 |
139 | it "should record edit in the journal" do
140 | j = journal.length
141 | double_click_paragraph
142 | replace_and_save("The [[quick brown]] fox.")
143 | journal.length.should == j+1
144 | end
145 | end
146 |
147 | def use_fixture_pages(*pages)
148 | `rm -rf #{TestDirs::TEST_DATA_DIR}`
149 | pages.each do |page|
150 | FileUtils.mkdir_p "#{TestDirs::TEST_DATA_DIR}/pages/"
151 | FileUtils.cp "#{TestDirs::FIXTURE_DATA_DIR}/pages/#{page}", "#{TestDirs::TEST_DATA_DIR}/pages/#{page}"
152 | end
153 | end
154 |
155 | describe "completely empty (but valid json) page" do
156 | before do
157 | use_fixture_pages("empty-page")
158 | visit("/view/empty-page")
159 | end
160 |
161 | it "should have a title of empty" do
162 | body.should include(" empty")
163 | end
164 |
165 | it "should have an empty story" do
166 | body.should include("")
167 | end
168 |
169 | it "should have an empty journal" do
170 | body.should include("")
171 | page.all(".journal .action").length.should == 0
172 | end
173 | end
174 |
175 |
176 | describe "moving paragraphs" do
177 | before do
178 | use_fixture_pages("multiple-paragraphs")
179 | end
180 |
181 | include IntegrationHelpers
182 |
183 | def move_paragraph
184 | page.load_test_library!
185 | first_paragraph.drag_down(2)
186 | end
187 |
188 | def journal_items
189 | page.all(".journal .action")
190 | end
191 |
192 | before do
193 | visit "/view/multiple-paragraphs"
194 | end
195 |
196 | it "should move paragraph 1 past paragraph 2" do
197 | move_paragraph
198 | page.all(".paragraph").map(&:text).should == ["paragraph 2", "paragraph 1", "paragraph 3"]
199 | end
200 |
201 | it "should add a move to the journal" do
202 | original_journal_length = journal_items.length
203 | move_paragraph
204 | journal_items.length.should == original_journal_length + 1
205 | journal_items.last[:class].should == "action move"
206 | end
207 |
208 |
209 | end
210 |
211 | describe "moving paragraphs between pages on different servers" do
212 | before do
213 | use_fixture_pages "simple-page", "multiple-paragraphs"
214 | remote = "localhost:#{Capybara.server_port}"
215 | visit "/view/simple-page/#{remote}/multiple-paragraphs"
216 | end
217 |
218 | def drag_item_to(item, destination)
219 | page.driver.browser.execute_script "(function(p, d) {
220 | var paragraph = $(p);
221 | var destination = $(d);
222 |
223 | var source = paragraph.parents('.story');
224 |
225 | paragraph.appendTo(destination);
226 |
227 | var ui = {item: paragraph};
228 | destination.trigger('sortupdate', [ui]);
229 | source.trigger('sortupdate', [ui]);
230 | }).apply(this, arguments);", item.native, destination.find(".story").native
231 | end
232 |
233 | def journal_for(page)
234 | JSON.parse(Net::HTTP.get(URI.parse("http://localhost:#{Capybara.server_port}/#{page}.json")))['journal']
235 | end
236 |
237 | it "should move the paragraph and add provenance to the journal" do
238 | pending
239 | local_page, remote_page = page.all(".page")
240 | paragraph_to_copy = remote_page.find(".item")
241 |
242 | drag_item_to paragraph_to_copy, local_page
243 |
244 | journal_entry = journal_for("simple-page").last
245 |
246 | journal_entry['type'].should == "add"
247 | journal_entry['item']['text'] == paragraph_to_copy.text
248 | journal_entry['origin'].should == {
249 | 'site' => "localhost:#{Capybara.server_port}",
250 | 'slug' => 'multiple-paragraphs'
251 | }
252 | end
253 |
254 | it "should move the paragraph from one to another" do
255 | pending
256 | local_page, remote_page = page.all(".page")
257 | paragraph_to_copy = remote_page.find(".item")
258 |
259 | drag_item_to paragraph_to_copy, local_page
260 |
261 | journal_for("multiple-paragraphs").each {|j| p j }
262 | journal_for("multiple-paragraphs").last['type'].should == 'remove'
263 | end
264 |
265 | end
266 |
267 | describe "navigating between pages" do
268 | before do
269 | visit("/")
270 | end
271 |
272 | def link_titled(text)
273 | page.all("a").select {|l| l.text == text}.first
274 | end
275 |
276 | it "should open internal links by adding a new wiki page to the web page" do
277 | link_titled("Local Editing").click
278 | page.all(".page").length.should == 2
279 | end
280 |
281 | it "should remove added pages when the browser's back button is pressed" do
282 | link_titled("Local Editing").click
283 | page.back
284 | page.all(".page").length.should == 1
285 | end
286 | end
287 |
288 | # This should probably be moved somewhere else.
289 | describe "should retrieve favicon" do
290 |
291 | def default_favicon
292 | File.join(APP_ROOT, "default-data/status/favicon.png")
293 | end
294 |
295 | def local_favicon
296 | File.join(TestDirs::TEST_DATA_DIR, "status/favicon.png")
297 | end
298 |
299 | def favicon_response
300 | Net::HTTP.get_response URI.parse(page.driver.rack_server.url("/favicon.png"))
301 | end
302 |
303 | def sha(text)
304 | Digest::SHA1.hexdigest(text)
305 | end
306 |
307 | it "should create an image when no other image is present" do
308 | File.exist?(local_favicon).should == false
309 | sha(favicon_response.body).should == sha(File.read(local_favicon))
310 | favicon_response['Content-Type'].should == 'image/png'
311 | end
312 |
313 | it "should return the local image when it exists" do
314 | FileUtils.mkdir_p File.dirname(local_favicon)
315 | FileUtils.cp "#{TestDirs::ROOT}/spec/favicon.png", local_favicon
316 | sha(favicon_response.body).should == sha(File.read(local_favicon))
317 | favicon_response['Content-Type'].should == 'image/png'
318 | end
319 |
320 | end
321 |
322 | describe "viewing journal" do
323 | before do
324 | use_fixture_pages("multiple-paragraphs", "duplicate-paragraphs")
325 | end
326 | include IntegrationHelpers
327 |
328 | RSpec::Matchers.define :be_highlighted do
329 | match do |actual|
330 | actual['class'].include?("target")
331 | end
332 | end
333 |
334 | it "should highlight a paragraph when hovering over journal entry" do
335 | visit "/view/multiple-paragraphs"
336 | paragraphs = page.all(".paragraph")
337 | first_paragraph = paragraphs.first
338 | other_paragraphs = paragraphs - [first_paragraph]
339 |
340 | paragraphs.each {|p| p.should_not be_highlighted }
341 |
342 | journal.first.roll_over
343 | first_paragraph.should be_highlighted
344 | other_paragraphs.each {|p| p.should_not be_highlighted }
345 |
346 | journal.first.roll_out
347 | paragraphs.each {|p| p.should_not be_highlighted }
348 | end
349 |
350 | it "should highlight all paragraphs with all the same JSON id" do
351 | visit "/view/duplicate-paragraphs"
352 | first_paragraph, second_paragraph = page.all(".paragraph")
353 |
354 | journal.first.roll_over
355 | first_paragraph.should be_highlighted
356 | second_paragraph.should be_highlighted
357 | end
358 | end
359 |
360 | # describe "testing javascript with mocha" do
361 |
362 | # it "should run with no failures" do
363 | # visit "/runtests.html"
364 | # failures = page.all(".failures em").first.text
365 | # trouble = page.all(".fail h2").collect{|e|e.text}.inspect
366 | # if failures.to_i > 0
367 | # puts "Paused to review #{failures} Mocha errors. RETURN to continue."
368 | # STDIN.readline
369 | # end
370 | # failures.should be('0'), trouble
371 | # end
372 | # end
373 |
--------------------------------------------------------------------------------
/client/js/underscore-min.js:
--------------------------------------------------------------------------------
1 | // Underscore.js 1.2.3
2 | // (c) 2009-2011 Jeremy Ashkenas, DocumentCloud Inc.
3 | // Underscore is freely distributable under the MIT license.
4 | // Portions of Underscore are inspired or borrowed from Prototype,
5 | // Oliver Steele's Functional, and John Resig's Micro-Templating.
6 | // For all details and documentation:
7 | // http://documentcloud.github.com/underscore
8 | (function(){function r(a,c,d){if(a===c)return a!==0||1/a==1/c;if(a==null||c==null)return a===c;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return false;switch(e){case "[object String]":return a==String(c);case "[object Number]":return a!=+a?c!=+c:a==0?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source==
9 | c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if(typeof a!="object"||typeof c!="object")return false;for(var f=d.length;f--;)if(d[f]==a)return true;d.push(a);var f=0,g=true;if(e=="[object Array]"){if(f=a.length,g=f==c.length)for(;f--;)if(!(g=f in a==f in c&&r(a[f],c[f],d)))break}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return false;for(var h in a)if(m.call(a,h)&&(f++,!(g=m.call(c,h)&&r(a[h],c[h],d))))break;if(g){for(h in c)if(m.call(c,
10 | h)&&!f--)break;g=!f}}d.pop();return g}var s=this,F=s._,o={},k=Array.prototype,p=Object.prototype,i=k.slice,G=k.concat,H=k.unshift,l=p.toString,m=p.hasOwnProperty,v=k.forEach,w=k.map,x=k.reduce,y=k.reduceRight,z=k.filter,A=k.every,B=k.some,q=k.indexOf,C=k.lastIndexOf,p=Array.isArray,I=Object.keys,t=Function.prototype.bind,b=function(a){return new n(a)};if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports)exports=module.exports=b;exports._=b}else typeof define==="function"&&
11 | define.amd?define("underscore",function(){return b}):s._=b;b.VERSION="1.2.3";var j=b.each=b.forEach=function(a,c,b){if(a!=null)if(v&&a.forEach===v)a.forEach(c,b);else if(a.length===+a.length)for(var e=0,f=a.length;e
2;a==null&&(a=[]);if(x&&a.reduce===x)return e&&(c=b.bind(c,e)),f?a.reduce(c,d):a.reduce(c);j(a,function(a,b,i){f?d=c.call(e,d,a,b,i):(d=a,f=true)});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(y&&a.reduceRight===y)return e&&(c=b.bind(c,e)),f?a.reduceRight(c,d):a.reduceRight(c);var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,
13 | c,d,e):b.reduce(g,c)};b.find=b.detect=function(a,c,b){var e;D(a,function(a,g,h){if(c.call(b,a,g,h))return e=a,true});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(z&&a.filter===z)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(A&&a.every===A)return a.every(c,
14 | b);j(a,function(a,g,h){if(!(e=e&&c.call(b,a,g,h)))return o});return e};var D=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(B&&a.some===B)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return o});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;return q&&a.indexOf===q?a.indexOf(c)!=-1:b=D(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(c.call?c||a:a[c]).apply(a,
15 | d)})};b.pluck=function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a))return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;bd?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=
17 | function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e>1;d(a[g])=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1));return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=
24 | function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=I||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var b=[],d;for(d in a)m.call(a,d)&&(b[b.length]=d);return b};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)b[d]!==void 0&&(a[d]=b[d])});return a};b.defaults=function(a){j(i.call(arguments,
25 | 1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(m.call(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===
26 | Object(a)};b.isArguments=function(a){return l.call(a)=="[object Arguments]"};if(!b.isArguments(arguments))b.isArguments=function(a){return!(!a||!m.call(a,"callee"))};b.isFunction=function(a){return l.call(a)=="[object Function]"};b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)==
27 | "[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.noConflict=function(){s._=F;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.mixin=function(a){j(b.functions(a),function(c){J(c,
28 | b[c]=a[c])})};var K=0;b.uniqueId=function(a){var b=K++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};b.template=function(a,c){var d=b.templateSettings,d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.escape,function(a,b){return"',_.escape("+b.replace(/\\'/g,"'")+"),'"}).replace(d.interpolate,function(a,b){return"',"+b.replace(/\\'/g,
29 | "'")+",'"}).replace(d.evaluate||null,function(a,b){return"');"+b.replace(/\\'/g,"'").replace(/[\r\n\t]/g," ")+";__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');",e=new Function("obj","_",d);return c?e(c,b):function(a){return e.call(this,a,b)}};var n=function(a){this._wrapped=a};b.prototype=n.prototype;var u=function(a,c){return c?b(a).chain():a},J=function(a,c){n.prototype[a]=function(){var a=i.call(arguments);H.call(a,this._wrapped);return u(c.apply(b,
30 | a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];n.prototype[a]=function(){b.apply(this._wrapped,arguments);return u(this._wrapped,this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];n.prototype[a]=function(){return u(b.apply(this._wrapped,arguments),this._chain)}});n.prototype.chain=function(){this._chain=true;return this};n.prototype.value=function(){return this._wrapped}}).call(this);
31 |
--------------------------------------------------------------------------------
/default-data/pages/welcome-visitors:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Welcome Visitors",
3 | "story": [
4 | {
5 | "text": "Welcome to the [[Smallest Federated Wiki]]. From this page you can find who we are and what we do. New sites provide this information and then claim the site as their own. You will need your own site to participate.",
6 | "id": "7b56f22a4b9ee974",
7 | "type": "paragraph"
8 | },
9 | {
10 | "type": "paragraph",
11 | "id": "821827c99b90cfd1",
12 | "text": "Pages about us."
13 | },
14 | {
15 | "type": "factory",
16 | "id": "63ad2e58eecdd9e5",
17 | "prompt": "Create a page about yourself. Start by making a link to that page right here. Double-click the gray box below. That opens an editor. Type your name enclosed in double square brackets. Then press Command/ALT-S to save."
18 | },
19 | {
20 | "type": "paragraph",
21 | "id": "2bbd646ff3f44b51",
22 | "text": "Pages where we do and share."
23 | },
24 | {
25 | "type": "factory",
26 | "id": "05e2fa92643677ca",
27 | "prompt": "Create a page about things you do on this wiki. Double-click the gray box below. Type a descriptive name of something you will be writing about. Enclose it in square brackets. Then press Command/ALT-S to save."
28 | },
29 | {
30 | "type": "paragraph",
31 | "id": "0cbb4ef5f5d7e472",
32 | "text": "Look for the claim button below this page. If you have your own OpenID you can use it to claim these pages so that only you can edit them. If you have a Google account you can use that too. Press the (G) button. Press the (Y) button to use your Yahoo account. You get the idea."
33 | },
34 | {
35 | "type": "paragraph",
36 | "id": "ee416d431ebf4fb4",
37 | "text": "You can edit your copy of these pages. Press [+] to add more writing spaces. Read [[How to Wiki]] for more ideas. Follow [[Recent Changes]] here and nearby."
38 | }
39 | ],
40 | "journal": [
41 | {
42 | "type": "create",
43 | "id": "7b56f22a30118509",
44 | "date": 1309114800000,
45 | "item": {
46 | "title": "Welcome Visitors"
47 | }
48 | },
49 | {
50 | "id": "7b56f22a4b9ee974",
51 | "type": "edit",
52 | "date": 1309114800000,
53 | "item": {
54 | "text": "Welcome to the [[Smallest Federated Wiki]]. This page was first drafted Sunday, June 26th, 2011, at [[Indie Web Camp]]. You are welcome to copy this page to any server you own and revise its welcoming message as you see fit. You can assume this has happened many times already.",
55 | "id": "7b56f22a4b9ee974",
56 | "type": "paragraph"
57 | }
58 | },
59 | {
60 | "type": "edit",
61 | "id": "7b56f22a4b9ee974",
62 | "item": {
63 | "text": "Welcome to the [[Smallest Federated Wiki]]. You may be seeing this page because you have just entered a wiki of your own. If so, you have three things to do before you go on.",
64 | "id": "7b56f22a4b9ee974",
65 | "type": "paragraph"
66 | },
67 | "date": 1344306124590
68 | },
69 | {
70 | "type": "add",
71 | "item": {
72 | "type": "paragraph",
73 | "id": "821827c99b90cfd1",
74 | "text": "One: Create a page about yourself. Start by making a link to that page right here. Double-click the gray box below. That opens an editor. Type your name enclosed in double square brackets. Then press Command/ALT-S to save."
75 | },
76 | "after": "7b56f22a4b9ee974",
77 | "id": "821827c99b90cfd1",
78 | "date": 1344306133455
79 | },
80 | {
81 | "type": "add",
82 | "item": {
83 | "type": "factory",
84 | "id": "63ad2e58eecdd9e5"
85 | },
86 | "after": "821827c99b90cfd1",
87 | "id": "63ad2e58eecdd9e5",
88 | "date": 1344306138935
89 | },
90 | {
91 | "type": "add",
92 | "item": {
93 | "type": "paragraph",
94 | "id": "2bbd646ff3f44b51",
95 | "text": "Two: Create a page about things you do on this wiki. Double-click the gray box below. Type a descriptive name of something you will be writing about. Enclose it in square brackets. Then press Command/ALT-S to save."
96 | },
97 | "after": "63ad2e58eecdd9e5",
98 | "id": "2bbd646ff3f44b51",
99 | "date": 1344306143742
100 | },
101 | {
102 | "type": "add",
103 | "item": {
104 | "type": "factory",
105 | "id": "05e2fa92643677ca"
106 | },
107 | "after": "2bbd646ff3f44b51",
108 | "id": "05e2fa92643677ca",
109 | "date": 1344306148580
110 | },
111 | {
112 | "type": "add",
113 | "item": {
114 | "type": "paragraph",
115 | "id": "0cbb4ef5f5d7e472",
116 | "text": "Three: Look for the claim button below this page. If you have your own OpenID you can use it to claim these pages so that only you can edit them. If you have a Google account you can use that too. Press the (G) button. Press the (Y) button to use your Yahoo account. You get the idea."
117 | },
118 | "after": "05e2fa92643677ca",
119 | "id": "0cbb4ef5f5d7e472",
120 | "date": 1344306151782
121 | },
122 | {
123 | "type": "add",
124 | "item": {
125 | "type": "paragraph",
126 | "id": "ee416d431ebf4fb4",
127 | "text": "Start writing. Click either link. Press [+] to add more writing spaces. Read [[How to Wiki]] for more ideas. Track changes with [[Recent Changes]] and [[Local Editing]]."
128 | },
129 | "after": "0cbb4ef5f5d7e472",
130 | "id": "ee416d431ebf4fb4",
131 | "date": 1344306168918
132 | },
133 | {
134 | "type": "edit",
135 | "id": "821827c99b90cfd1",
136 | "item": {
137 | "type": "paragraph",
138 | "id": "821827c99b90cfd1",
139 | "text": "One: A page about myself."
140 | },
141 | "date": 1361751371185
142 | },
143 | {
144 | "type": "edit",
145 | "id": "2bbd646ff3f44b51",
146 | "item": {
147 | "type": "paragraph",
148 | "id": "2bbd646ff3f44b51",
149 | "text": "Two: Pages about what we do here."
150 | },
151 | "date": 1361751496155
152 | },
153 | {
154 | "type": "edit",
155 | "id": "821827c99b90cfd1",
156 | "item": {
157 | "type": "paragraph",
158 | "id": "821827c99b90cfd1",
159 | "text": "One: Pages about us."
160 | },
161 | "date": 1361751512280
162 | },
163 | {
164 | "type": "edit",
165 | "id": "7b56f22a4b9ee974",
166 | "item": {
167 | "text": "Welcome to the [[Smallest Federated Wiki]]. From this page you can find who we are and what we do.",
168 | "id": "7b56f22a4b9ee974",
169 | "type": "paragraph"
170 | },
171 | "date": 1361751649467
172 | },
173 | {
174 | "type": "edit",
175 | "id": "7b56f22a4b9ee974",
176 | "item": {
177 | "text": "Welcome to the [[Smallest Federated Wiki]]. From this introductory page you can find who we are and what we do.",
178 | "id": "7b56f22a4b9ee974",
179 | "type": "paragraph"
180 | },
181 | "date": 1361751711554
182 | },
183 | {
184 | "type": "edit",
185 | "id": "2bbd646ff3f44b51",
186 | "item": {
187 | "type": "paragraph",
188 | "id": "2bbd646ff3f44b51",
189 | "text": "Two: Pages where we do and share."
190 | },
191 | "date": 1361751811972
192 | },
193 | {
194 | "type": "edit",
195 | "id": "7b56f22a4b9ee974",
196 | "item": {
197 | "text": "Welcome to the [[Smallest Federated Wiki]]. From this page you can find who we are and what we do. New sites provide this information and then claim the site as their own. You will need your own site to participate.",
198 | "id": "7b56f22a4b9ee974",
199 | "type": "paragraph"
200 | },
201 | "date": 1361751965824
202 | },
203 | {
204 | "type": "edit",
205 | "id": "821827c99b90cfd1",
206 | "item": {
207 | "type": "paragraph",
208 | "id": "821827c99b90cfd1",
209 | "text": "Pages about us."
210 | },
211 | "date": 1361752195146
212 | },
213 | {
214 | "type": "edit",
215 | "id": "2bbd646ff3f44b51",
216 | "item": {
217 | "type": "paragraph",
218 | "id": "2bbd646ff3f44b51",
219 | "text": "Pages where we do and share."
220 | },
221 | "date": 1361752202873
222 | },
223 | {
224 | "type": "edit",
225 | "id": "0cbb4ef5f5d7e472",
226 | "item": {
227 | "type": "paragraph",
228 | "id": "0cbb4ef5f5d7e472",
229 | "text": "Look for the claim button below this page. If you have your own OpenID you can use it to claim these pages so that only you can edit them. If you have a Google account you can use that too. Press the (G) button. Press the (Y) button to use your Yahoo account. You get the idea."
230 | },
231 | "date": 1361752223856
232 | },
233 | {
234 | "type": "edit",
235 | "id": "ee416d431ebf4fb4",
236 | "item": {
237 | "type": "paragraph",
238 | "id": "ee416d431ebf4fb4",
239 | "text": "Click either link. Press [+] to add more writing spaces. Read [[How to Wiki]] for more ideas. Track changes with [[Recent Changes]] and [[Local Editing]]."
240 | },
241 | "date": 1361752251367
242 | },
243 | {
244 | "type": "edit",
245 | "id": "ee416d431ebf4fb4",
246 | "item": {
247 | "type": "paragraph",
248 | "id": "ee416d431ebf4fb4",
249 | "text": "You can your copy of these pages. Press [+] to add more writing spaces. Read [[How to Wiki]] for more ideas. Discover [[Recent Changes]] here and nearby."
250 | },
251 | "date": 1361752421212
252 | },
253 | {
254 | "type": "edit",
255 | "id": "ee416d431ebf4fb4",
256 | "item": {
257 | "type": "paragraph",
258 | "id": "ee416d431ebf4fb4",
259 | "text": "You can your copy of these pages. Press [+] to add more writing spaces."
260 | },
261 | "date": 1361752436556
262 | },
263 | {
264 | "item": {
265 | "type": "paragraph",
266 | "id": "78a8278db93c6ed2",
267 | "text": "Read [[How to Wiki]] for more ideas. Discover [[Recent Changes]] here and nearby."
268 | },
269 | "id": "78a8278db93c6ed2",
270 | "type": "add",
271 | "after": "ee416d431ebf4fb4",
272 | "date": 1361752437061
273 | },
274 | {
275 | "type": "edit",
276 | "id": "78a8278db93c6ed2",
277 | "item": {
278 | "type": "paragraph",
279 | "id": "78a8278db93c6ed2",
280 | "text": "Read [[How to Wiki]] for more ideas."
281 | },
282 | "date": 1361752441155
283 | },
284 | {
285 | "item": {
286 | "type": "paragraph",
287 | "id": "67a126ec849b55ed",
288 | "text": "Discover [[Recent Changes]] here and nearby."
289 | },
290 | "id": "67a126ec849b55ed",
291 | "type": "add",
292 | "after": "78a8278db93c6ed2",
293 | "date": 1361752441668
294 | },
295 | {
296 | "type": "remove",
297 | "id": "78a8278db93c6ed2",
298 | "date": 1361752452012
299 | },
300 | {
301 | "type": "edit",
302 | "id": "ee416d431ebf4fb4",
303 | "item": {
304 | "type": "paragraph",
305 | "id": "ee416d431ebf4fb4",
306 | "text": "You can your copy of these pages. Press [+] to add more writing spaces. Read [[How to Wiki]] for more ideas."
307 | },
308 | "date": 1361752459193
309 | },
310 | {
311 | "type": "remove",
312 | "id": "67a126ec849b55ed",
313 | "date": 1361752461793
314 | },
315 | {
316 | "type": "edit",
317 | "id": "ee416d431ebf4fb4",
318 | "item": {
319 | "type": "paragraph",
320 | "id": "ee416d431ebf4fb4",
321 | "text": "You can your copy of these pages. Press [+] to add more writing spaces. Read [[How to Wiki]] for more ideas. Follow [[Recent Changes]] here and nearby."
322 | },
323 | "date": 1361752483992
324 | },
325 | {
326 | "type": "edit",
327 | "id": "ee416d431ebf4fb4",
328 | "item": {
329 | "type": "paragraph",
330 | "id": "ee416d431ebf4fb4",
331 | "text": "You can edit your copy of these pages. Press [+] to add more writing spaces. Read [[How to Wiki]] for more ideas. Follow [[Recent Changes]] here and nearby."
332 | },
333 | "date": 1361752498310
334 | }
335 | ]
336 | }
--------------------------------------------------------------------------------
/client/js/jquery.ie.cors.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2011 Ovea
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | /**
17 | * https://gist.github.com/1114981
18 | *
19 | * By default, support transferring session cookie with XDomainRequest for IE. The cookie value is by default 'jsessionid'
20 | *
21 | * You can change the session cookie value like this, before including this script:
22 | *
23 | * window.XDR_SESSION_COOKIE_NAME = 'ID';
24 | *
25 | * Or if you want to disable cookie session support:
26 | *
27 | * window.XDR_SESSION_COOKIE_NAME = null;
28 | *
29 | * If you need to convert other cookies as headers:
30 | *
31 | * window.XDR_COOKIE_HEADERS = ['PHP_SESSION'];
32 | *
33 | * To DEBUG:
34 | *
35 | * window.XDR_DEBUG = true;
36 | *
37 | * To pass some headers:
38 | *
39 | * window.XDR_HEADERS = ['Content-Type', 'Accept']
40 | *
41 | */
42 | (function ($) {
43 |
44 | if (!('__jquery_xdomain__' in $)
45 | && $.browser.msie // must be IE
46 | && 'XDomainRequest' in window // and support XDomainRequest (IE8+)
47 | && !(window.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()) // and must not support CORS (IE10+)
48 | && document.location.href.indexOf("file:///") == -1) { // and must not be local
49 |
50 | $['__jquery_xdomain__'] = $.support.cors = true;
51 |
52 | var urlMatcher = /^(((([^:\/#\?]+:)?(?:\/\/((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?]+)(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/,
53 | oldxhr = $.ajaxSettings.xhr,
54 | sessionCookie = 'XDR_SESSION_COOKIE_NAME' in window ? window['XDR_SESSION_COOKIE_NAME'] : "jsessionid",
55 | cookies = 'XDR_COOKIE_HEADERS' in window ? window['XDR_COOKIE_HEADERS'] : [],
56 | headers = 'XDR_HEADERS' in window ? window['XDR_HEADERS'] : ['Content-Type'],
57 | ReadyState = {UNSENT:0, OPENED:1, LOADING:3, DONE:4},
58 | debug = window['XDR_DEBUG'] && 'console' in window,
59 | XDomainRequestAdapter,
60 | domain,
61 | reqId = 0;
62 |
63 | function forEachCookie(names, fn) {
64 | if (typeof names == 'string') {
65 | names = [names];
66 | }
67 | var i, cookie;
68 | for (i = 0; i < names.length; i++) {
69 | cookie = new RegExp('(?:^|; )' + names[i] + '=([^;]*)', 'i').exec(document.cookie);
70 | cookie = cookie && cookie[1];
71 | if (cookie) {
72 | fn.call(null, names[i], cookie);
73 | }
74 | }
75 | }
76 |
77 | function parseResponse(str) {
78 | // str === [data][header]~status~hlen~
79 | // min: ~0~0~
80 | if (str.length >= 5) {
81 | // return[0] = status
82 | // return[1] = data
83 | // return[2] = header
84 | var sub = str.substring(str.length <= 20 ? 0 : str.length - 20),
85 | i = sub.length - 1,
86 | end, hl, st;
87 | if (sub.charAt(i) === '~') {
88 | for (end = i--; i >= 0 && sub.charAt(i) !== '~'; i--);
89 | hl = parseInt(sub.substring(i + 1, end));
90 | if (!isNaN(hl) && hl >= 0 && i >= 2 && sub.charAt(i) === '~') {
91 | for (end = i--; i >= 0 && sub.charAt(i) !== '~'; i--);
92 | st = parseInt(sub.substring(i + 1, end));
93 | if (!isNaN(st) && i >= 0 && sub.charAt(i) === '~') {
94 | end = str.length - hl - sub.length + i;
95 | return [st, str.substring(0, end), str.substr(end, hl)];
96 | }
97 | }
98 | }
99 | }
100 | return [200, str, ''];
101 | }
102 |
103 | function parseUrl(url) {
104 | if (typeof(url) === "object") {
105 | return url;
106 | }
107 | var matches = urlMatcher.exec(url);
108 | return matches ? {
109 | href:matches[0] || "",
110 | hrefNoHash:matches[1] || "",
111 | hrefNoSearch:matches[2] || "",
112 | domain:matches[3] || "",
113 | protocol:matches[4] || "",
114 | authority:matches[5] || "",
115 | username:matches[7] || "",
116 | password:matches[8] || "",
117 | host:matches[9] || "",
118 | hostname:matches[10] || "",
119 | port:matches[11] || "",
120 | pathname:matches[12] || "",
121 | directory:matches[13] || "",
122 | filename:matches[14] || "",
123 | search:matches[15] || "",
124 | hash:matches[16] || ""
125 | } : {};
126 | }
127 |
128 | function parseCookies(header) {
129 | if (header.length == 0) {
130 | return [];
131 | }
132 | var cooks = [], i = 0, start = 0, end, dom;
133 | do {
134 | end = header.indexOf(',', start);
135 | cooks[i] = (cooks[i] || '') + header.substring(start, end == -1 ? header.length : end);
136 | start = end + 1;
137 | if (cooks[i].indexOf('Expires=') == -1 || cooks[i].indexOf(',') != -1) {
138 | i++;
139 | } else {
140 | cooks[i] += ',';
141 | }
142 | } while (end > 0);
143 | for (i = 0; i < cooks.length; i++) {
144 | dom = cooks[i].indexOf('Domain=');
145 | if (dom != -1) {
146 | cooks[i] = cooks[i].substring(0, dom) + cooks[i].substring(cooks[i].indexOf(';', dom) + 1);
147 | }
148 | }
149 | return cooks;
150 | }
151 |
152 | domain = parseUrl(document.location.href).domain;
153 | XDomainRequestAdapter = function () {
154 | var self = this,
155 | _xdr = new XDomainRequest(),
156 | _mime,
157 | _reqHeaders = [],
158 | _method,
159 | _url,
160 | _id = reqId++,
161 | _setState = function (state) {
162 | self.readyState = state;
163 | if (typeof self.onreadystatechange === 'function') {
164 | self.onreadystatechange.call(self);
165 | }
166 | },
167 | _done = function (state, code) {
168 | if (!self.responseText) {
169 | self.responseText = '';
170 | }
171 | if (debug) {
172 | console.log('[XDR-' + _id + '] request end with state ' + state + ' and code ' + code + ' and data length ' + self.responseText.length);
173 | }
174 | self.status = code;
175 | if (!self.responseType) {
176 | _mime = _mime || _xdr.contentType;
177 | if (_mime.match(/\/json/)) {
178 | self.responseType = 'json';
179 | self.response = self.responseText;
180 | } else if (_mime.match(/\/xml/)) {
181 | self.responseType = 'document';
182 | var $error, dom = new ActiveXObject('Microsoft.XMLDOM');
183 | dom.async = false;
184 | dom.loadXML(self.responseText);
185 | self.responseXML = self.response = dom;
186 | if ($(dom).children('error').length != 0) {
187 | $error = $(dom).find('error');
188 | self.status = parseInt($error.attr('response_code'));
189 | }
190 | } else {
191 | self.responseType = 'text';
192 | self.response = self.responseText;
193 | }
194 | }
195 | _setState(state);
196 | // clean memory
197 | _xdr = null;
198 | _reqHeaders = null;
199 | _url = null;
200 | };
201 | _xdr.onprogress = function () {
202 | _setState(ReadyState.LOADING);
203 | };
204 | _xdr.ontimeout = function () {
205 | _done(ReadyState.DONE, 408);
206 | };
207 | _xdr.onerror = function () {
208 | _done(ReadyState.DONE, 500);
209 | };
210 | _xdr.onload = function () {
211 | // check if we are using a filter which modify the response
212 | var cooks, i, resp = parseResponse(_xdr.responseText || '');
213 | if (debug) {
214 | console.log('[XDR-' + reqId + '] parsing cookies for header ' + resp[2]);
215 | }
216 | cooks = parseCookies(resp[2]);
217 | self.responseText = resp[1] || '';
218 | if (debug) {
219 | console.log('[XDR-' + _id + '] raw data:\n' + _xdr.responseText + '\n parsed response: status=' + resp[0] + ', header=' + resp[2] + ', data=\n' + resp[1]);
220 | }
221 | for (i = 0; i < cooks.length; i++) {
222 | if (debug) {
223 | console.log('[XDR-' + _id + '] installing cookie ' + cooks[i]);
224 | }
225 | document.cookie = cooks[i] + ";Domain=" + document.domain;
226 | }
227 | _done(ReadyState.DONE, resp[0]);
228 | resp = null;
229 | };
230 | this.readyState = ReadyState.UNSENT;
231 | this.status = 0;
232 | this.statusText = '';
233 | this.responseType = '';
234 | this.timeout = 0;
235 | this.withCredentials = false;
236 | this.overrideMimeType = function (mime) {
237 | _mime = mime;
238 | };
239 | this.abort = function () {
240 | _xdr.abort();
241 | };
242 | this.setRequestHeader = function (k, v) {
243 | if ($.inArray(k, headers) >= 0) {
244 | _reqHeaders.push({k:k, v:v});
245 | }
246 | };
247 | this.open = function (m, u) {
248 | _url = u;
249 | _method = m;
250 | _setState(ReadyState.OPENED);
251 | };
252 | this.send = function (data) {
253 | _xdr.timeout = this.timeout;
254 | if (sessionCookie || cookies || _reqHeaders.length) {
255 | var h, addParam = function (name, value) {
256 | var q = _url.indexOf('?');
257 | _url += (q == -1 ? '?' : '&') + name + '=' + encodeURIComponent(value);
258 | if (debug) {
259 | console.log('[XDR-' + _id + '] added parameter ' + name + "=" + value + " => " + _url);
260 | }
261 | };
262 | for (h = 0; h < _reqHeaders.length; h++) {
263 | addParam(_reqHeaders[h].k, _reqHeaders[h].v);
264 | }
265 | forEachCookie(sessionCookie, function (name, value) {
266 | var q = _url.indexOf('?');
267 | if (q == -1) {
268 | _url += ';' + name + '=' + value;
269 | } else {
270 | _url = _url.substring(0, q) + ';' + name + '=' + value + _url.substring(q);
271 | }
272 | if (debug) {
273 | console.log('[XDR-' + _id + '] added cookie ' + _url);
274 | }
275 | });
276 | forEachCookie(cookies, addParam);
277 | addParam('_xdr', '' + _id);
278 | }
279 | if (debug) {
280 | console.log('[XDR-' + _id + '] opening ' + _url);
281 | }
282 | _xdr.open(_method, _url);
283 | if (debug) {
284 | console.log('[XDR-' + _id + '] send, timeout=' + _xdr.timeout);
285 | }
286 | _xdr.send(data);
287 | };
288 | this.getAllResponseHeaders = function () {
289 | return '';
290 | };
291 | this.getResponseHeader = function () {
292 | return null;
293 | }
294 | };
295 |
296 | $.ajaxSettings.xhr = function () {
297 | var target = parseUrl(this.url).domain;
298 | if (target === "" || target === domain) {
299 | return oldxhr.call($.ajaxSettings);
300 | } else {
301 | try {
302 | return new XDomainRequestAdapter();
303 | } catch (e) {
304 | }
305 | }
306 | };
307 |
308 | }
309 | })
310 | (jQuery);
--------------------------------------------------------------------------------
/server/Wikiduino/Wikiduino.ino:
--------------------------------------------------------------------------------
1 |
2 | // Copyright (c) 2011, Ward Cunningham
3 | // Released under MIT and GPLv2
4 |
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #define num(array) (sizeof(array)/sizeof(array[0]))
11 |
12 | // pin assignments
13 | byte radioPowerPin = 2;
14 |
15 | // Ethernet Configuration
16 |
17 | byte mac[] = { 0xEE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
18 | IPAddress ip(10, 94, 54, 2);
19 | IPAddress gateway(10, 94, 54, 1);
20 |
21 | //IPAddress ip(10, 0, 3, 201 );
22 | //IPAddress gateway( 10, 0, 3, 1 );
23 |
24 | //IPAddress ip(192, 168, 0, 201 );
25 | //IPAddress gateway( 192, 168, 0, 1 );
26 |
27 | IPAddress subnet( 255, 255, 255, 0 );
28 |
29 | EthernetServer server(1111);
30 | EthernetClient client(255);
31 |
32 | unsigned long requests = 0;
33 | unsigned long lastRequest = 0; // records the request number at time of the most recent radio powerup
34 | byte radioPowerMode = 1; // indicates which power management algorith to use
35 |
36 | // Sensor Configuration
37 |
38 | OneWire ds(8);
39 |
40 | int analog[3];
41 | struct Temp {
42 | unsigned int code;
43 | int data;
44 | } temp[4] = {{0,0}};
45 |
46 | unsigned int last = 100;
47 | unsigned int powersave = 0;
48 | unsigned long lastSample = 100;
49 | unsigned long lastRadioOn = 0; // records time the radio was last powered on
50 | unsigned long totalRadioOn = 0; // records total time the radio has been on
51 | unsigned long now = 0;
52 | unsigned long rollOvers = 0;
53 | boolean topOfHourFlag = false;
54 | unsigned long topOfHour = 0;
55 | boolean radioOn = false; // status of radio power
56 | unsigned long crc_errs = 0;
57 |
58 | // Arduino Setup and Loop
59 |
60 | void setup() {
61 | Serial.begin(115200L);
62 | Ethernet.begin(mac, ip, gateway, subnet);
63 | server.begin();
64 | // configure radio power control pin
65 | pinMode(radioPowerPin,OUTPUT);
66 | powerRadio(true);
67 | }
68 |
69 | void loop() {
70 | sample(); // every second or so
71 | pinMode(13,OUTPUT);
72 | digitalWrite(13,HIGH);
73 | serve(); // whenever web requests come in
74 | digitalWrite(13,LOW);
75 | }
76 |
77 | // Sample and Hold Analog and One-Wire Temperature Data
78 |
79 | void sample() {
80 | now = millis();
81 | if ((now-lastSample) >= 1000) {
82 | if(now < lastSample) {
83 | rollOvers++;
84 | }
85 | lastSample = now;
86 | manageRadioPower();
87 | analogSample();
88 | tempSample();
89 | }
90 | }
91 |
92 | unsigned long modeOneOnTime = (58*60+30) * 1000UL;
93 | unsigned long modeOneOffTime = (4*60+15) * 1000UL;
94 | unsigned long modeTwoOnTime = (2*60+30) * 1000UL;
95 | unsigned long modeTwoOffTime = (4*60+5) * 1000UL;
96 | unsigned long longestOnTimeWithoutRequest = 3600*1000UL;
97 |
98 | void manageRadioPower() {
99 | if(radioOn && (lastRequest == requests) && ((now-lastRadioOn) >= longestOnTimeWithoutRequest)) {
100 | // radio has been on for a while, but received no requests, may be wedged, try rebooting
101 | printTime(now,0); Serial.println(" Resetting radio");
102 | powerRadio(false);
103 | delay(2000);
104 | now = millis();
105 | powerRadio(true);
106 | } else {
107 | if(radioPowerMode == 0 || !topOfHourFlag) { // stay on
108 | if(!radioOn) {
109 | powerRadio(true);
110 | }
111 | } else {
112 | // remove integer hours from time, just interested in phase ... not needed if we get a sync often enough relative to wrapping
113 | while((now-topOfHour) > (3600*1000UL)) {
114 | topOfHour += 3600*1000UL;
115 | }
116 | unsigned long timeAfterHour = (now-topOfHour) % (3600*1000UL);
117 | if(radioPowerMode == 1) { // on at 58m30s, off at 4m15s after hour
118 | boolean duringOffTime = (timeAfterHour > modeOneOffTime) && (timeAfterHour < modeOneOnTime);
119 | if(radioOn && duringOffTime) {
120 | powerRadio(false);
121 | } else if (!radioOn && !duringOffTime) {
122 | powerRadio(true);
123 | }
124 | } else if(radioPowerMode == 2) { // minimal radio uptime on at 2m30s, off at 4m5s after hour
125 | boolean duringOnTime = (timeAfterHour > modeTwoOnTime) && (timeAfterHour < modeTwoOffTime);
126 | if(radioOn && !duringOnTime) {
127 | powerRadio(false);
128 | } else if (!radioOn && duringOnTime) {
129 | powerRadio(true);
130 | }
131 | }
132 | }
133 | }
134 | }
135 |
136 | void printTime(unsigned long t,unsigned long ref) {
137 | unsigned long hour;
138 | unsigned long minute;
139 | unsigned long second;
140 |
141 | t -= ref;
142 | hour = t / (3600 * 1000UL);
143 | minute = t % (3600 * 1000UL);
144 | second = minute % (60 * 1000UL);
145 | minute -= second;
146 | if(topOfHourFlag) {
147 | Serial.print("Sync'd: ");
148 | }
149 | Serial.print(hour); Serial.print(":");
150 | Serial.print(minute/60000UL); Serial.print(":");
151 | Serial.print(second/1000.0,3);
152 | }
153 |
154 | float uptime() { // returns uptime as a floating point hour
155 | return (4294967296.0 * rollOvers + now) / (3600.0 * 1000);
156 | }
157 |
158 | float radioOnTime() { // returns time radio has been on in hours
159 | return (float) (totalRadioOn + (radioOn ? (now-lastRadioOn) : 0)) / (3600.0 * 1000);
160 | }
161 |
162 | void powerRadio(boolean power) {
163 | digitalWrite(radioPowerPin,power);
164 | radioOn = power;
165 | if(power) {
166 | lastRadioOn = now;
167 | lastRequest = requests;
168 | } else {
169 | totalRadioOn += (now-lastRadioOn);
170 | lastRadioOn = 0;
171 | }
172 | printTime(now,0); Serial.print(" "); printTime(now,topOfHour); Serial.print(" "); Serial.println(radioOn);
173 | }
174 |
175 | void analogSample() {
176 | for (int i = 0; i < num(analog); i++) {
177 | analog[i] = analogRead(i);
178 | }
179 | }
180 |
181 | byte data[12];
182 | unsigned int id;
183 | int ch = -1;
184 |
185 | void tempSample() {
186 | finishTempSample();
187 | startTempSample();
188 | }
189 |
190 | void startTempSample() {
191 | if (ch < 0) {
192 | ds.reset_search();
193 | }
194 | if (!ds.search(data)) {
195 | ch = -1;
196 | }
197 | else {
198 | if (OneWire::crc8(data, 7) == data[7] && 0x28 == data[0]) {
199 | id = data[2]*256u+data[1];
200 | ch = channel (id);
201 | ds.reset();
202 | ds.select(data);
203 | ds.write(0x44,1); // start conversion, with parasite power on at the end
204 | } else {
205 | crc_errs++;
206 | Serial.print(id);
207 | Serial.println(F(" a-err"));
208 | }
209 | }
210 | }
211 |
212 | void finishTempSample() {
213 | if (ch >= 0) { // if we've discovered a devise and started a conversion
214 | ds.reset();
215 | ds.select(data);
216 | ds.write(0xBE); // Read Scratchpad
217 | for (int i = 0; i < 9; i++) {
218 | data[i] = ds.read();
219 | }
220 | if (OneWire::crc8(data, 8) == data[8]) {
221 | temp[ch].data = data[1]*256+data[0];
222 | temp[ch].code = id; // don't set this too early or we could report bad data
223 | } else {
224 | crc_errs++;
225 | Serial.print(id);
226 | Serial.println(F(" d-err"));
227 | }
228 | }
229 | }
230 |
231 | int channel(int id) {
232 | for (int ch=0; ch")); }
309 | void scpt(__FlashStringHelper* s) { p(F("")); }
310 | void stag(__FlashStringHelper* s) { p('<'); p(s); p('>'); }
311 | void etag(__FlashStringHelper* s) { p('<'); p('/'); p(s); p('>'); }
312 |
313 | void htmlReport () {
314 | code(F("200 OK"));
315 | mime(F("text/html"));
316 | stag(F("html"));
317 | stag(F("head"));
318 | link(F("style.css"));
319 | scpt(F("js/jquery.min.js"));
320 | scpt(F("js/jquery-ui.custom.min.js"));
321 | scpt(F("client.js"));
322 | etag(F("head"));
323 | stag(F("body"));
324 | p(F(""));
325 | p(F("
"));
326 | etag(F("div"));
327 | etag(F("div"));
328 | etag(F("body"));
329 | etag(F("html"));
330 | }
331 |
332 | boolean more;
333 |
334 | void sh () { if (more) { p(','); } p('{'); more = false; }
335 | void sa () { if (more) { p(','); } p('['); more = false; }
336 | void eh () { p('}'); more = true; }
337 | void ea () { p(']'); more = true; }
338 | void k (__FlashStringHelper* s) { if (more) { p(','); } p('"'); p(s); p('"'); p(':'); more = false; }
339 | void v (__FlashStringHelper* s) { if (more) { p(','); } p('"'); p(s); p('"'); more = true; }
340 | void v (long s) { if (more) { p(','); } client.print(s); more = true; }
341 | void v (int s) { if (more) { p(','); } client.print(s); more = true; }
342 | void v (float s) { if (more) { p(','); } client.print(s); more = true; }
343 |
344 | void jsonReport () {
345 | more = false;
346 | long id = 472647400L;
347 |
348 | code(F("200 OK"));
349 | mime(F("application/json"));
350 | sh();
351 | k(F("title")); v(F("garden-report"));
352 | k(F("logo"));
353 | sh();
354 | k(F("nw")); sa(); v(127); v(255); v(127); ea();
355 | k(F("se")); sa(); v(63); v(63); v(16); ea();
356 | eh();
357 | k(F("story"));
358 | sa();
359 | sh();
360 | k(F("type")); v(F("paragraph"));
361 | k(F("id")); v(id++);
362 | k(F("text")); v(F("Experimental data from Nike's Community Garden. This content is being served on the open-source hardware Arduino platform running the [[smallest-federated-wiki]] server application."));
363 | eh();
364 | for (int ch=0; chUpdated in Seconds"));
370 | k(F("data"));
371 | sa();
372 | sa(); v(1314306006L); v(temp[ch].data * (9.0F/5/16) + 32); ea();
373 | ea();
374 | eh();
375 | }
376 | for (int ch=0; chVolts")) : ch == 2 ? v(F("Solar Panel
Volts")) : v(F("Daylight
Percent")));
381 | k(F("data"));
382 | sa();
383 | sa(); v(1314306006L); v(analog[ch] * (ch>=1 ? (1347.0F/89.45F/1024) : (100.0F/1024))); ea();
384 | ea();
385 | eh();
386 | }
387 | sh();
388 | k(F("type")); v(F("chart"));
389 | k(F("id")); v(id++);
390 | k(F("caption")); v(F("Wiki Server
Requests"));
391 | k(F("data"));
392 | sa();
393 | sa(); v(1314306006L); v((long)requests); ea();
394 | ea();
395 | eh();
396 | sh();
397 | k(F("type")); v(F("chart"));
398 | k(F("id")); v(id++);
399 | k(F("caption")); v(F("Arduino Uptime
Hours"));
400 | k(F("data"));
401 | sa();
402 | sa(); v(1314306006L); v(uptime()); ea();
403 | ea();
404 | eh();
405 | sh();
406 | k(F("type")); v(F("chart"));
407 | k(F("id")); v(id++);
408 | k(F("caption")); v(F("Radio Uptime
Hours"));
409 | k(F("data"));
410 | sa();
411 | sa(); v(1314306006L); v(radioOnTime()); ea();
412 | ea();
413 | eh();
414 | sh();
415 | k(F("type")); v(F("chart"));
416 | k(F("id")); v(id++);
417 | k(F("caption")); v(F("Minutes After Hour"));
418 | k(F("data"));
419 | sa();
420 | sa(); v(1314306006L); v((float) (topOfHourFlag ? ((now-topOfHour) % (3600*1000UL))/60000.0 : 0.0)); ea();
421 | ea();
422 | eh();
423 | sh();
424 | k(F("type")); v(F("chart"));
425 | k(F("id")); v(id++);
426 | k(F("caption")); v(F("Radio Power Mode"));
427 | k(F("data"));
428 | sa();
429 | sa(); v(1314306006L); v(radioPowerMode); ea();
430 | ea();
431 | eh();
432 | ea();
433 | k(F("journal"));
434 | sa();
435 | ea();
436 | eh();
437 | n(F(""));
438 | }
439 |
440 | void errorReport () {
441 | code(F("404 Not Found"));
442 | mime(F("text/html"));
443 | n(F("404 Not Found"));
444 | }
445 |
446 | void faviconReport () {
447 | code(F("200 OK"));
448 | mime(F("image/png"));
449 | client.print(F("\0211\0120\0116\0107\015\012\032\012\0\0\0\015\0111\0110\0104\0122\0\0\0\05\0\0\0\010\010\02\0\0\0\0276\0223\0242\0154\0\0\0\025\0111\0104\0101\0124\010\0231\0143\0374\0377\0277\0203\01\011\0260\074\0370\0372\0236\0236\0174\0\0366\0225\026\0\0105\030\0216\0134\0\0\0\0\0111\0105\0116\0104\0256\0102\0140\0202"));
450 | }
451 |
--------------------------------------------------------------------------------