##// END OF EJS Templates
REST API for creating/updating wiki pages (#7082)....
Jean-Philippe Lang -
r10505:eff874b29a90
parent child
Show More
@@ -1,575 +1,580
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'uri'
19 19 require 'cgi'
20 20
21 21 class Unauthorized < Exception; end
22 22
23 23 class ApplicationController < ActionController::Base
24 24 include Redmine::I18n
25 25
26 26 class_attribute :accept_api_auth_actions
27 27 class_attribute :accept_rss_auth_actions
28 28 class_attribute :model_object
29 29
30 30 layout 'base'
31 31
32 32 protect_from_forgery
33 33 def handle_unverified_request
34 34 super
35 35 cookies.delete(:autologin)
36 36 end
37 37
38 38 before_filter :session_expiration, :user_setup, :check_if_login_required, :set_localization
39 39
40 40 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
41 41 rescue_from ::Unauthorized, :with => :deny_access
42 42 rescue_from ::ActionView::MissingTemplate, :with => :missing_template
43 43
44 44 include Redmine::Search::Controller
45 45 include Redmine::MenuManager::MenuController
46 46 helper Redmine::MenuManager::MenuHelper
47 47
48 48 def session_expiration
49 49 if session[:user_id]
50 50 if session_expired? && !try_to_autologin
51 51 reset_session
52 52 flash[:error] = l(:error_session_expired)
53 53 redirect_to signin_url
54 54 else
55 55 session[:atime] = Time.now.utc.to_i
56 56 end
57 57 end
58 58 end
59 59
60 60 def session_expired?
61 61 if Setting.session_lifetime?
62 62 unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60)
63 63 return true
64 64 end
65 65 end
66 66 if Setting.session_timeout?
67 67 unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60)
68 68 return true
69 69 end
70 70 end
71 71 false
72 72 end
73 73
74 74 def start_user_session(user)
75 75 session[:user_id] = user.id
76 76 session[:ctime] = Time.now.utc.to_i
77 77 session[:atime] = Time.now.utc.to_i
78 78 end
79 79
80 80 def user_setup
81 81 # Check the settings cache for each request
82 82 Setting.check_cache
83 83 # Find the current user
84 84 User.current = find_current_user
85 85 logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
86 86 end
87 87
88 88 # Returns the current user or nil if no user is logged in
89 89 # and starts a session if needed
90 90 def find_current_user
91 91 user = nil
92 92 unless api_request?
93 93 if session[:user_id]
94 94 # existing session
95 95 user = (User.active.find(session[:user_id]) rescue nil)
96 96 elsif autologin_user = try_to_autologin
97 97 user = autologin_user
98 98 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
99 99 # RSS key authentication does not start a session
100 100 user = User.find_by_rss_key(params[:key])
101 101 end
102 102 end
103 103 if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
104 104 if (key = api_key_from_request)
105 105 # Use API key
106 106 user = User.find_by_api_key(key)
107 107 else
108 108 # HTTP Basic, either username/password or API key/random
109 109 authenticate_with_http_basic do |username, password|
110 110 user = User.try_to_login(username, password) || User.find_by_api_key(username)
111 111 end
112 112 end
113 113 # Switch user if requested by an admin user
114 114 if user && user.admin? && (username = api_switch_user_from_request)
115 115 su = User.find_by_login(username)
116 116 if su && su.active?
117 117 logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger
118 118 user = su
119 119 else
120 120 render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
121 121 end
122 122 end
123 123 end
124 124 user
125 125 end
126 126
127 127 def try_to_autologin
128 128 if cookies[:autologin] && Setting.autologin?
129 129 # auto-login feature starts a new session
130 130 user = User.try_to_autologin(cookies[:autologin])
131 131 if user
132 132 reset_session
133 133 start_user_session(user)
134 134 end
135 135 user
136 136 end
137 137 end
138 138
139 139 # Sets the logged in user
140 140 def logged_user=(user)
141 141 reset_session
142 142 if user && user.is_a?(User)
143 143 User.current = user
144 144 start_user_session(user)
145 145 else
146 146 User.current = User.anonymous
147 147 end
148 148 end
149 149
150 150 # Logs out current user
151 151 def logout_user
152 152 if User.current.logged?
153 153 cookies.delete :autologin
154 154 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
155 155 self.logged_user = nil
156 156 end
157 157 end
158 158
159 159 # check if login is globally required to access the application
160 160 def check_if_login_required
161 161 # no check needed if user is already logged in
162 162 return true if User.current.logged?
163 163 require_login if Setting.login_required?
164 164 end
165 165
166 166 def set_localization
167 167 lang = nil
168 168 if User.current.logged?
169 169 lang = find_language(User.current.language)
170 170 end
171 171 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
172 172 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
173 173 if !accept_lang.blank?
174 174 accept_lang = accept_lang.downcase
175 175 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
176 176 end
177 177 end
178 178 lang ||= Setting.default_language
179 179 set_language_if_valid(lang)
180 180 end
181 181
182 182 def require_login
183 183 if !User.current.logged?
184 184 # Extract only the basic url parameters on non-GET requests
185 185 if request.get?
186 186 url = url_for(params)
187 187 else
188 188 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
189 189 end
190 190 respond_to do |format|
191 191 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
192 192 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
193 193 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
194 194 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
195 195 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
196 196 end
197 197 return false
198 198 end
199 199 true
200 200 end
201 201
202 202 def require_admin
203 203 return unless require_login
204 204 if !User.current.admin?
205 205 render_403
206 206 return false
207 207 end
208 208 true
209 209 end
210 210
211 211 def deny_access
212 212 User.current.logged? ? render_403 : require_login
213 213 end
214 214
215 215 # Authorize the user for the requested action
216 216 def authorize(ctrl = params[:controller], action = params[:action], global = false)
217 217 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
218 218 if allowed
219 219 true
220 220 else
221 221 if @project && @project.archived?
222 222 render_403 :message => :notice_not_authorized_archived_project
223 223 else
224 224 deny_access
225 225 end
226 226 end
227 227 end
228 228
229 229 # Authorize the user for the requested action outside a project
230 230 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
231 231 authorize(ctrl, action, global)
232 232 end
233 233
234 234 # Find project of id params[:id]
235 235 def find_project
236 236 @project = Project.find(params[:id])
237 237 rescue ActiveRecord::RecordNotFound
238 238 render_404
239 239 end
240 240
241 241 # Find project of id params[:project_id]
242 242 def find_project_by_project_id
243 243 @project = Project.find(params[:project_id])
244 244 rescue ActiveRecord::RecordNotFound
245 245 render_404
246 246 end
247 247
248 248 # Find a project based on params[:project_id]
249 249 # TODO: some subclasses override this, see about merging their logic
250 250 def find_optional_project
251 251 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
252 252 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
253 253 allowed ? true : deny_access
254 254 rescue ActiveRecord::RecordNotFound
255 255 render_404
256 256 end
257 257
258 258 # Finds and sets @project based on @object.project
259 259 def find_project_from_association
260 260 render_404 unless @object.present?
261 261
262 262 @project = @object.project
263 263 end
264 264
265 265 def find_model_object
266 266 model = self.class.model_object
267 267 if model
268 268 @object = model.find(params[:id])
269 269 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
270 270 end
271 271 rescue ActiveRecord::RecordNotFound
272 272 render_404
273 273 end
274 274
275 275 def self.model_object(model)
276 276 self.model_object = model
277 277 end
278 278
279 279 # Filter for bulk issue operations
280 280 def find_issues
281 281 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
282 282 raise ActiveRecord::RecordNotFound if @issues.empty?
283 283 if @issues.detect {|issue| !issue.visible?}
284 284 deny_access
285 285 return
286 286 end
287 287 @projects = @issues.collect(&:project).compact.uniq
288 288 @project = @projects.first if @projects.size == 1
289 289 rescue ActiveRecord::RecordNotFound
290 290 render_404
291 291 end
292 292
293 293 # make sure that the user is a member of the project (or admin) if project is private
294 294 # used as a before_filter for actions that do not require any particular permission on the project
295 295 def check_project_privacy
296 296 if @project && !@project.archived?
297 297 if @project.visible?
298 298 true
299 299 else
300 300 deny_access
301 301 end
302 302 else
303 303 @project = nil
304 304 render_404
305 305 false
306 306 end
307 307 end
308 308
309 309 def back_url
310 310 url = params[:back_url]
311 311 if url.nil? && referer = request.env['HTTP_REFERER']
312 312 url = CGI.unescape(referer.to_s)
313 313 end
314 314 url
315 315 end
316 316
317 317 def redirect_back_or_default(default)
318 318 back_url = params[:back_url].to_s
319 319 if back_url.present?
320 320 begin
321 321 uri = URI.parse(back_url)
322 322 # do not redirect user to another host or to the login or register page
323 323 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
324 324 redirect_to(back_url)
325 325 return
326 326 end
327 327 rescue URI::InvalidURIError
328 328 logger.warn("Could not redirect to invalid URL #{back_url}")
329 329 # redirect to default
330 330 end
331 331 end
332 332 redirect_to default
333 333 false
334 334 end
335 335
336 336 # Redirects to the request referer if present, redirects to args or call block otherwise.
337 337 def redirect_to_referer_or(*args, &block)
338 338 redirect_to :back
339 339 rescue ::ActionController::RedirectBackError
340 340 if args.any?
341 341 redirect_to *args
342 342 elsif block_given?
343 343 block.call
344 344 else
345 345 raise "#redirect_to_referer_or takes arguments or a block"
346 346 end
347 347 end
348 348
349 349 def render_403(options={})
350 350 @project = nil
351 351 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
352 352 return false
353 353 end
354 354
355 355 def render_404(options={})
356 356 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
357 357 return false
358 358 end
359 359
360 360 # Renders an error response
361 361 def render_error(arg)
362 362 arg = {:message => arg} unless arg.is_a?(Hash)
363 363
364 364 @message = arg[:message]
365 365 @message = l(@message) if @message.is_a?(Symbol)
366 366 @status = arg[:status] || 500
367 367
368 368 respond_to do |format|
369 369 format.html {
370 370 render :template => 'common/error', :layout => use_layout, :status => @status
371 371 }
372 372 format.any { head @status }
373 373 end
374 374 end
375 375
376 376 # Handler for ActionView::MissingTemplate exception
377 377 def missing_template
378 378 logger.warn "Missing template, responding with 404"
379 379 @project = nil
380 380 render_404
381 381 end
382 382
383 383 # Filter for actions that provide an API response
384 384 # but have no HTML representation for non admin users
385 385 def require_admin_or_api_request
386 386 return true if api_request?
387 387 if User.current.admin?
388 388 true
389 389 elsif User.current.logged?
390 390 render_error(:status => 406)
391 391 else
392 392 deny_access
393 393 end
394 394 end
395 395
396 396 # Picks which layout to use based on the request
397 397 #
398 398 # @return [boolean, string] name of the layout to use or false for no layout
399 399 def use_layout
400 400 request.xhr? ? false : 'base'
401 401 end
402 402
403 403 def invalid_authenticity_token
404 404 if api_request?
405 405 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
406 406 end
407 407 render_error "Invalid form authenticity token."
408 408 end
409 409
410 410 def render_feed(items, options={})
411 411 @items = items || []
412 412 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
413 413 @items = @items.slice(0, Setting.feeds_limit.to_i)
414 414 @title = options[:title] || Setting.app_title
415 415 render :template => "common/feed", :formats => [:atom], :layout => false,
416 416 :content_type => 'application/atom+xml'
417 417 end
418 418
419 419 def self.accept_rss_auth(*actions)
420 420 if actions.any?
421 421 self.accept_rss_auth_actions = actions
422 422 else
423 423 self.accept_rss_auth_actions || []
424 424 end
425 425 end
426 426
427 427 def accept_rss_auth?(action=action_name)
428 428 self.class.accept_rss_auth.include?(action.to_sym)
429 429 end
430 430
431 431 def self.accept_api_auth(*actions)
432 432 if actions.any?
433 433 self.accept_api_auth_actions = actions
434 434 else
435 435 self.accept_api_auth_actions || []
436 436 end
437 437 end
438 438
439 439 def accept_api_auth?(action=action_name)
440 440 self.class.accept_api_auth.include?(action.to_sym)
441 441 end
442 442
443 443 # Returns the number of objects that should be displayed
444 444 # on the paginated list
445 445 def per_page_option
446 446 per_page = nil
447 447 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
448 448 per_page = params[:per_page].to_s.to_i
449 449 session[:per_page] = per_page
450 450 elsif session[:per_page]
451 451 per_page = session[:per_page]
452 452 else
453 453 per_page = Setting.per_page_options_array.first || 25
454 454 end
455 455 per_page
456 456 end
457 457
458 458 # Returns offset and limit used to retrieve objects
459 459 # for an API response based on offset, limit and page parameters
460 460 def api_offset_and_limit(options=params)
461 461 if options[:offset].present?
462 462 offset = options[:offset].to_i
463 463 if offset < 0
464 464 offset = 0
465 465 end
466 466 end
467 467 limit = options[:limit].to_i
468 468 if limit < 1
469 469 limit = 25
470 470 elsif limit > 100
471 471 limit = 100
472 472 end
473 473 if offset.nil? && options[:page].present?
474 474 offset = (options[:page].to_i - 1) * limit
475 475 offset = 0 if offset < 0
476 476 end
477 477 offset ||= 0
478 478
479 479 [offset, limit]
480 480 end
481 481
482 482 # qvalues http header parser
483 483 # code taken from webrick
484 484 def parse_qvalues(value)
485 485 tmp = []
486 486 if value
487 487 parts = value.split(/,\s*/)
488 488 parts.each {|part|
489 489 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
490 490 val = m[1]
491 491 q = (m[2] or 1).to_f
492 492 tmp.push([val, q])
493 493 end
494 494 }
495 495 tmp = tmp.sort_by{|val, q| -q}
496 496 tmp.collect!{|val, q| val}
497 497 end
498 498 return tmp
499 499 rescue
500 500 nil
501 501 end
502 502
503 503 # Returns a string that can be used as filename value in Content-Disposition header
504 504 def filename_for_content_disposition(name)
505 505 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
506 506 end
507 507
508 508 def api_request?
509 509 %w(xml json).include? params[:format]
510 510 end
511 511
512 512 # Returns the API key present in the request
513 513 def api_key_from_request
514 514 if params[:key].present?
515 515 params[:key].to_s
516 516 elsif request.headers["X-Redmine-API-Key"].present?
517 517 request.headers["X-Redmine-API-Key"].to_s
518 518 end
519 519 end
520 520
521 521 # Returns the API 'switch user' value if present
522 522 def api_switch_user_from_request
523 523 request.headers["X-Redmine-Switch-User"].to_s.presence
524 524 end
525 525
526 526 # Renders a warning flash if obj has unsaved attachments
527 527 def render_attachment_warning_if_needed(obj)
528 528 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
529 529 end
530 530
531 531 # Sets the `flash` notice or error based the number of issues that did not save
532 532 #
533 533 # @param [Array, Issue] issues all of the saved and unsaved Issues
534 534 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
535 535 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
536 536 if unsaved_issue_ids.empty?
537 537 flash[:notice] = l(:notice_successful_update) unless issues.empty?
538 538 else
539 539 flash[:error] = l(:notice_failed_to_save_issues,
540 540 :count => unsaved_issue_ids.size,
541 541 :total => issues.size,
542 542 :ids => '#' + unsaved_issue_ids.join(', #'))
543 543 end
544 544 end
545 545
546 546 # Rescues an invalid query statement. Just in case...
547 547 def query_statement_invalid(exception)
548 548 logger.error "Query::StatementInvalid: #{exception.message}" if logger
549 549 session.delete(:query)
550 550 sort_clear if respond_to?(:sort_clear)
551 551 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
552 552 end
553 553
554 554 # Renders a 200 response for successfull updates or deletions via the API
555 555 def render_api_ok
556 # head :ok would return a response body with one space
557 render :text => '', :status => :ok, :layout => nil
556 render_api_head :ok
557 end
558
559 # Renders a head API response
560 def render_api_head(status)
561 # #head would return a response body with one space
562 render :text => '', :status => status, :layout => nil
558 563 end
559 564
560 565 # Renders API response on validation failure
561 566 def render_validation_errors(objects)
562 567 if objects.is_a?(Array)
563 568 @error_messages = objects.map {|object| object.errors.full_messages}.flatten
564 569 else
565 570 @error_messages = objects.errors.full_messages
566 571 end
567 572 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
568 573 end
569 574
570 575 # Overrides #_include_layout? so that #render with no arguments
571 576 # doesn't use the layout for api requests
572 577 def _include_layout?(*args)
573 578 api_request? ? false : super
574 579 end
575 580 end
@@ -1,335 +1,359
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'diff'
19 19
20 20 # The WikiController follows the Rails REST controller pattern but with
21 21 # a few differences
22 22 #
23 23 # * index - shows a list of WikiPages grouped by page or date
24 24 # * new - not used
25 25 # * create - not used
26 26 # * show - will also show the form for creating a new wiki page
27 27 # * edit - used to edit an existing or new page
28 28 # * update - used to save a wiki page update to the database, including new pages
29 29 # * destroy - normal
30 30 #
31 31 # Other member and collection methods are also used
32 32 #
33 33 # TODO: still being worked on
34 34 class WikiController < ApplicationController
35 35 default_search_scope :wiki_pages
36 36 before_filter :find_wiki, :authorize
37 37 before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
38 38 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version]
39 accept_api_auth :index, :show
39 accept_api_auth :index, :show, :update
40 40
41 41 helper :attachments
42 42 include AttachmentsHelper
43 43 helper :watchers
44 44 include Redmine::Export::PDF
45 45
46 46 # List of pages, sorted alphabetically and by parent (hierarchy)
47 47 def index
48 48 load_pages_for_index
49 49
50 50 respond_to do |format|
51 51 format.html {
52 52 @pages_by_parent_id = @pages.group_by(&:parent_id)
53 53 }
54 54 format.api
55 55 end
56 56 end
57 57
58 58 # List of page, by last update
59 59 def date_index
60 60 load_pages_for_index
61 61 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
62 62 end
63 63
64 64 # display a page (in editing mode if it doesn't exist)
65 65 def show
66 66 if @page.new_record?
67 67 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request?
68 68 edit
69 69 render :action => 'edit'
70 70 else
71 71 render_404
72 72 end
73 73 return
74 74 end
75 75 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
76 76 deny_access
77 77 return
78 78 end
79 79 @content = @page.content_for_version(params[:version])
80 80 if User.current.allowed_to?(:export_wiki_pages, @project)
81 81 if params[:format] == 'pdf'
82 82 send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
83 83 return
84 84 elsif params[:format] == 'html'
85 85 export = render_to_string :action => 'export', :layout => false
86 86 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
87 87 return
88 88 elsif params[:format] == 'txt'
89 89 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
90 90 return
91 91 end
92 92 end
93 93 @editable = editable?
94 94 @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
95 95 @content.current_version? &&
96 96 Redmine::WikiFormatting.supports_section_edit?
97 97
98 98 respond_to do |format|
99 99 format.html
100 100 format.api
101 101 end
102 102 end
103 103
104 104 # edit an existing page or a new one
105 105 def edit
106 106 return render_403 unless editable?
107 107 if @page.new_record?
108 108 @page.content = WikiContent.new(:page => @page)
109 109 if params[:parent].present?
110 110 @page.parent = @page.wiki.find_page(params[:parent].to_s)
111 111 end
112 112 end
113 113
114 114 @content = @page.content_for_version(params[:version])
115 115 @content.text = initial_page_content(@page) if @content.text.blank?
116 116 # don't keep previous comment
117 117 @content.comments = nil
118 118
119 119 # To prevent StaleObjectError exception when reverting to a previous version
120 120 @content.version = @page.content.version
121 121
122 122 @text = @content.text
123 123 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
124 124 @section = params[:section].to_i
125 125 @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
126 126 render_404 if @text.blank?
127 127 end
128 128 end
129 129
130 130 # Creates a new page or updates an existing one
131 131 def update
132 132 return render_403 unless editable?
133 was_new_page = @page.new_record?
133 134 @page.content = WikiContent.new(:page => @page) if @page.new_record?
134 135 @page.safe_attributes = params[:wiki_page]
135 136
136 @content = @page.content_for_version(params[:version])
137 @content.text = initial_page_content(@page) if @content.text.blank?
138 # don't keep previous comment
139 @content.comments = nil
137 @content = @page.content
138 content_params = params[:content]
139 if content_params.nil? && params[:wiki_page].is_a?(Hash)
140 content_params = params[:wiki_page].slice(:text, :comments, :version)
141 end
142 content_params ||= {}
140 143
141 if !@page.new_record? && params[:content].present? && @content.text == params[:content][:text]
144 if !@page.new_record? && content_params.present? && @content.text == content_params[:text]
142 145 attachments = Attachment.attach_files(@page, params[:attachments])
143 146 render_attachment_warning_if_needed(@page)
144 147 # don't save content if text wasn't changed
145 148 @page.save
146 149 redirect_to :action => 'show', :project_id => @project, :id => @page.title
147 150 return
148 151 end
149 152
150 @content.comments = params[:content][:comments]
151 @text = params[:content][:text]
153 @content.comments = content_params[:comments]
154 @text = content_params[:text]
152 155 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
153 156 @section = params[:section].to_i
154 157 @section_hash = params[:section_hash]
155 158 @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
156 159 else
157 @content.version = params[:content][:version]
160 @content.version = content_params[:version] if content_params[:version]
158 161 @content.text = @text
159 162 end
160 163 @content.author = User.current
161 164 @page.content = @content
162 165 if @page.save
163 166 attachments = Attachment.attach_files(@page, params[:attachments])
164 167 render_attachment_warning_if_needed(@page)
165 168 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
166 redirect_to :action => 'show', :project_id => @project, :id => @page.title
169
170 respond_to do |format|
171 format.html { redirect_to :action => 'show', :project_id => @project, :id => @page.title }
172 format.api {
173 if was_new_page
174 render :action => 'show', :status => :created, :location => url_for(:controller => 'wiki', :action => 'show', :project_id => @project, :id => @page.title)
167 175 else
168 render :action => 'edit'
176 render_api_ok
177 end
178 }
179 end
180 else
181 respond_to do |format|
182 format.html { render :action => 'edit' }
183 format.api { render_validation_errors(@content) }
184 end
169 185 end
170 186
171 187 rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
172 188 # Optimistic locking exception
189 respond_to do |format|
190 format.html {
173 191 flash.now[:error] = l(:notice_locking_conflict)
174 192 render :action => 'edit'
193 }
194 format.api { render_api_head :conflict }
195 end
175 196 rescue ActiveRecord::RecordNotSaved
176 render :action => 'edit'
197 respond_to do |format|
198 format.html { render :action => 'edit' }
199 format.api { render_validation_errors(@content) }
200 end
177 201 end
178 202
179 203 # rename a page
180 204 def rename
181 205 return render_403 unless editable?
182 206 @page.redirect_existing_links = true
183 207 # used to display the *original* title if some AR validation errors occur
184 208 @original_title = @page.pretty_title
185 209 if request.post? && @page.update_attributes(params[:wiki_page])
186 210 flash[:notice] = l(:notice_successful_update)
187 211 redirect_to :action => 'show', :project_id => @project, :id => @page.title
188 212 end
189 213 end
190 214
191 215 def protect
192 216 @page.update_attribute :protected, params[:protected]
193 217 redirect_to :action => 'show', :project_id => @project, :id => @page.title
194 218 end
195 219
196 220 # show page history
197 221 def history
198 222 @version_count = @page.content.versions.count
199 223 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
200 224 # don't load text
201 225 @versions = @page.content.versions.find :all,
202 226 :select => "id, author_id, comments, updated_on, version",
203 227 :order => 'version DESC',
204 228 :limit => @version_pages.items_per_page + 1,
205 229 :offset => @version_pages.current.offset
206 230
207 231 render :layout => false if request.xhr?
208 232 end
209 233
210 234 def diff
211 235 @diff = @page.diff(params[:version], params[:version_from])
212 236 render_404 unless @diff
213 237 end
214 238
215 239 def annotate
216 240 @annotate = @page.annotate(params[:version])
217 241 render_404 unless @annotate
218 242 end
219 243
220 244 # Removes a wiki page and its history
221 245 # Children can be either set as root pages, removed or reassigned to another parent page
222 246 def destroy
223 247 return render_403 unless editable?
224 248
225 249 @descendants_count = @page.descendants.size
226 250 if @descendants_count > 0
227 251 case params[:todo]
228 252 when 'nullify'
229 253 # Nothing to do
230 254 when 'destroy'
231 255 # Removes all its descendants
232 256 @page.descendants.each(&:destroy)
233 257 when 'reassign'
234 258 # Reassign children to another parent page
235 259 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
236 260 return unless reassign_to
237 261 @page.children.each do |child|
238 262 child.update_attribute(:parent, reassign_to)
239 263 end
240 264 else
241 265 @reassignable_to = @wiki.pages - @page.self_and_descendants
242 266 return
243 267 end
244 268 end
245 269 @page.destroy
246 270 redirect_to :action => 'index', :project_id => @project
247 271 end
248 272
249 273 def destroy_version
250 274 return render_403 unless editable?
251 275
252 276 @content = @page.content_for_version(params[:version])
253 277 @content.destroy
254 278 redirect_to_referer_or :action => 'history', :id => @page.title, :project_id => @project
255 279 end
256 280
257 281 # Export wiki to a single pdf or html file
258 282 def export
259 283 @pages = @wiki.pages.all(:order => 'title', :include => [:content, :attachments], :limit => 75)
260 284 respond_to do |format|
261 285 format.html {
262 286 export = render_to_string :action => 'export_multiple', :layout => false
263 287 send_data(export, :type => 'text/html', :filename => "wiki.html")
264 288 }
265 289 format.pdf {
266 290 send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf")
267 291 }
268 292 end
269 293 end
270 294
271 295 def preview
272 296 page = @wiki.find_page(params[:id])
273 297 # page is nil when previewing a new page
274 298 return render_403 unless page.nil? || editable?(page)
275 299 if page
276 300 @attachements = page.attachments
277 301 @previewed = page.content
278 302 end
279 303 @text = params[:content][:text]
280 304 render :partial => 'common/preview'
281 305 end
282 306
283 307 def add_attachment
284 308 return render_403 unless editable?
285 309 attachments = Attachment.attach_files(@page, params[:attachments])
286 310 render_attachment_warning_if_needed(@page)
287 311 redirect_to :action => 'show', :id => @page.title, :project_id => @project
288 312 end
289 313
290 314 private
291 315
292 316 def find_wiki
293 317 @project = Project.find(params[:project_id])
294 318 @wiki = @project.wiki
295 319 render_404 unless @wiki
296 320 rescue ActiveRecord::RecordNotFound
297 321 render_404
298 322 end
299 323
300 324 # Finds the requested page or a new page if it doesn't exist
301 325 def find_existing_or_new_page
302 326 @page = @wiki.find_or_new_page(params[:id])
303 327 if @wiki.page_found_with_redirect?
304 328 redirect_to params.update(:id => @page.title)
305 329 end
306 330 end
307 331
308 332 # Finds the requested page and returns a 404 error if it doesn't exist
309 333 def find_existing_page
310 334 @page = @wiki.find_page(params[:id])
311 335 if @page.nil?
312 336 render_404
313 337 return
314 338 end
315 339 if @wiki.page_found_with_redirect?
316 340 redirect_to params.update(:id => @page.title)
317 341 end
318 342 end
319 343
320 344 # Returns true if the current user is allowed to edit the page, otherwise false
321 345 def editable?(page = @page)
322 346 page.editable_by?(User.current)
323 347 end
324 348
325 349 # Returns the default content of a new wiki page
326 350 def initial_page_content(page)
327 351 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
328 352 extend helper unless self.instance_of?(helper)
329 353 helper.instance_method(:initial_page_content).bind(self).call(page)
330 354 end
331 355
332 356 def load_pages_for_index
333 357 @pages = @wiki.pages.with_updated_on.order("#{WikiPage.table_name}.title").includes(:wiki => :project).includes(:parent).all
334 358 end
335 359 end
@@ -1,235 +1,235
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'diff'
19 19 require 'enumerator'
20 20
21 21 class WikiPage < ActiveRecord::Base
22 22 include Redmine::SafeAttributes
23 23
24 24 belongs_to :wiki
25 25 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
26 26 acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
27 27 acts_as_tree :dependent => :nullify, :order => 'title'
28 28
29 29 acts_as_watchable
30 30 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
31 31 :description => :text,
32 32 :datetime => :created_on,
33 33 :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}}
34 34
35 35 acts_as_searchable :columns => ['title', "#{WikiContent.table_name}.text"],
36 36 :include => [{:wiki => :project}, :content],
37 37 :permission => :view_wiki_pages,
38 38 :project_key => "#{Wiki.table_name}.project_id"
39 39
40 40 attr_accessor :redirect_existing_links
41 41
42 42 validates_presence_of :title
43 43 validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
44 44 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
45 45 validates_associated :content
46 46
47 47 validate :validate_parent_title
48 48 before_destroy :remove_redirects
49 49 before_save :handle_redirects
50 50
51 51 # eager load information about last updates, without loading text
52 52 scope :with_updated_on, {
53 53 :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on, #{WikiContent.table_name}.version",
54 54 :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id"
55 55 }
56 56
57 57 # Wiki pages that are protected by default
58 58 DEFAULT_PROTECTED_PAGES = %w(sidebar)
59 59
60 safe_attributes 'parent_id',
60 safe_attributes 'parent_id', 'parent_title',
61 61 :if => lambda {|page, user| page.new_record? || user.allowed_to?(:rename_wiki_pages, page.project)}
62 62
63 63 def initialize(attributes=nil, *args)
64 64 super
65 65 if new_record? && DEFAULT_PROTECTED_PAGES.include?(title.to_s.downcase)
66 66 self.protected = true
67 67 end
68 68 end
69 69
70 70 def visible?(user=User.current)
71 71 !user.nil? && user.allowed_to?(:view_wiki_pages, project)
72 72 end
73 73
74 74 def title=(value)
75 75 value = Wiki.titleize(value)
76 76 @previous_title = read_attribute(:title) if @previous_title.blank?
77 77 write_attribute(:title, value)
78 78 end
79 79
80 80 def handle_redirects
81 81 self.title = Wiki.titleize(title)
82 82 # Manage redirects if the title has changed
83 83 if !@previous_title.blank? && (@previous_title != title) && !new_record?
84 84 # Update redirects that point to the old title
85 85 wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
86 86 r.redirects_to = title
87 87 r.title == r.redirects_to ? r.destroy : r.save
88 88 end
89 89 # Remove redirects for the new title
90 90 wiki.redirects.find_all_by_title(title).each(&:destroy)
91 91 # Create a redirect to the new title
92 92 wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
93 93 @previous_title = nil
94 94 end
95 95 end
96 96
97 97 def remove_redirects
98 98 # Remove redirects to this page
99 99 wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
100 100 end
101 101
102 102 def pretty_title
103 103 WikiPage.pretty_title(title)
104 104 end
105 105
106 106 def content_for_version(version=nil)
107 107 result = content.versions.find_by_version(version.to_i) if version
108 108 result ||= content
109 109 result
110 110 end
111 111
112 112 def diff(version_to=nil, version_from=nil)
113 113 version_to = version_to ? version_to.to_i : self.content.version
114 114 content_to = content.versions.find_by_version(version_to)
115 115 content_from = version_from ? content.versions.find_by_version(version_from.to_i) : content_to.previous
116 116
117 117 if content_from.version > content_to.version
118 118 content_to, content_from = content_from, content_to
119 119 end
120 120
121 121 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
122 122 end
123 123
124 124 def annotate(version=nil)
125 125 version = version ? version.to_i : self.content.version
126 126 c = content.versions.find_by_version(version)
127 127 c ? WikiAnnotate.new(c) : nil
128 128 end
129 129
130 130 def self.pretty_title(str)
131 131 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
132 132 end
133 133
134 134 def project
135 135 wiki.project
136 136 end
137 137
138 138 def text
139 139 content.text if content
140 140 end
141 141
142 142 def updated_on
143 143 unless @updated_on
144 144 if time = read_attribute(:updated_on)
145 145 # content updated_on was eager loaded with the page
146 146 begin
147 147 @updated_on = (self.class.default_timezone == :utc ? Time.parse(time.to_s).utc : Time.parse(time.to_s).localtime)
148 148 rescue
149 149 end
150 150 else
151 151 @updated_on = content && content.updated_on
152 152 end
153 153 end
154 154 @updated_on
155 155 end
156 156
157 157 # Returns true if usr is allowed to edit the page, otherwise false
158 158 def editable_by?(usr)
159 159 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
160 160 end
161 161
162 162 def attachments_deletable?(usr=User.current)
163 163 editable_by?(usr) && super(usr)
164 164 end
165 165
166 166 def parent_title
167 167 @parent_title || (self.parent && self.parent.pretty_title)
168 168 end
169 169
170 170 def parent_title=(t)
171 171 @parent_title = t
172 172 parent_page = t.blank? ? nil : self.wiki.find_page(t)
173 173 self.parent = parent_page
174 174 end
175 175
176 176 protected
177 177
178 178 def validate_parent_title
179 179 errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
180 180 errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
181 181 errors.add(:parent_title, :not_same_project) if parent && (parent.wiki_id != wiki_id)
182 182 end
183 183 end
184 184
185 185 class WikiDiff < Redmine::Helpers::Diff
186 186 attr_reader :content_to, :content_from
187 187
188 188 def initialize(content_to, content_from)
189 189 @content_to = content_to
190 190 @content_from = content_from
191 191 super(content_to.text, content_from.text)
192 192 end
193 193 end
194 194
195 195 class WikiAnnotate
196 196 attr_reader :lines, :content
197 197
198 198 def initialize(content)
199 199 @content = content
200 200 current = content
201 201 current_lines = current.text.split(/\r?\n/)
202 202 @lines = current_lines.collect {|t| [nil, nil, t]}
203 203 positions = []
204 204 current_lines.size.times {|i| positions << i}
205 205 while (current.previous)
206 206 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
207 207 d.each_slice(3) do |s|
208 208 sign, line = s[0], s[1]
209 209 if sign == '+' && positions[line] && positions[line] != -1
210 210 if @lines[positions[line]][0].nil?
211 211 @lines[positions[line]][0] = current.version
212 212 @lines[positions[line]][1] = current.author
213 213 end
214 214 end
215 215 end
216 216 d.each_slice(3) do |s|
217 217 sign, line = s[0], s[1]
218 218 if sign == '-'
219 219 positions.insert(line, -1)
220 220 else
221 221 positions[line] = nil
222 222 end
223 223 end
224 224 positions.compact!
225 225 # Stop if every line is annotated
226 226 break unless @lines.detect { |line| line[0].nil? }
227 227 current = current.previous
228 228 end
229 229 @lines.each { |line|
230 230 line[0] ||= current.version
231 231 # if the last known version is > 1 (eg. history was cleared), we don't know the author
232 232 line[1] ||= current.author if current.version == 1
233 233 }
234 234 end
235 235 end
@@ -1,85 +1,168
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../../test_helper', __FILE__)
19 19
20 20 class ApiTest::WikiPagesTest < ActionController::IntegrationTest
21 21 fixtures :projects, :users, :roles, :members, :member_roles,
22 22 :enabled_modules, :wikis, :wiki_pages, :wiki_contents,
23 23 :wiki_content_versions, :attachments
24 24
25 25 def setup
26 26 Setting.rest_api_enabled = '1'
27 27 end
28 28
29 29 test "GET /projects/:project_id/wiki/index.xml should return wiki pages" do
30 30 get '/projects/ecookbook/wiki/index.xml'
31 assert_response :success
31 assert_response 200
32 32 assert_equal 'application/xml', response.content_type
33 33 assert_select 'wiki_pages[type=array]' do
34 34 assert_select 'wiki_page', :count => Wiki.find(1).pages.count
35 35 assert_select 'wiki_page' do
36 36 assert_select 'title', :text => 'CookBook_documentation'
37 37 assert_select 'version', :text => '3'
38 38 assert_select 'created_on'
39 39 assert_select 'updated_on'
40 40 end
41 assert_select 'wiki_page' do
42 assert_select 'title', :text => 'Page_with_an_inline_image'
43 assert_select 'parent[title=?]', 'CookBook_documentation'
44 end
41 45 end
42 46 end
43 47
44 48 test "GET /projects/:project_id/wiki/:title.xml should return wiki page" do
45 49 get '/projects/ecookbook/wiki/CookBook_documentation.xml'
46 assert_response :success
50 assert_response 200
47 51 assert_equal 'application/xml', response.content_type
48 52 assert_select 'wiki_page' do
49 53 assert_select 'title', :text => 'CookBook_documentation'
50 54 assert_select 'version', :text => '3'
51 55 assert_select 'text'
52 56 assert_select 'author'
53 57 assert_select 'created_on'
54 58 assert_select 'updated_on'
55 59 end
56 60 end
57 61
58 62 test "GET /projects/:project_id/wiki/:title.xml with unknown title and edit permission should respond with 404" do
59 63 get '/projects/ecookbook/wiki/Invalid_Page.xml', {}, credentials('jsmith')
60 64 assert_response 404
61 65 assert_equal 'application/xml', response.content_type
62 66 end
63 67
64 68 test "GET /projects/:project_id/wiki/:title/:version.xml should return wiki page version" do
65 69 get '/projects/ecookbook/wiki/CookBook_documentation/2.xml'
66 assert_response :success
70 assert_response 200
67 71 assert_equal 'application/xml', response.content_type
68 72 assert_select 'wiki_page' do
69 73 assert_select 'title', :text => 'CookBook_documentation'
70 74 assert_select 'version', :text => '2'
71 75 assert_select 'text'
72 76 assert_select 'author'
73 77 assert_select 'created_on'
74 78 assert_select 'updated_on'
75 79 end
76 80 end
77 81
78 82 test "GET /projects/:project_id/wiki/:title/:version.xml without permission should be denied" do
79 83 Role.anonymous.remove_permission! :view_wiki_edits
80 84
81 85 get '/projects/ecookbook/wiki/CookBook_documentation/2.xml'
82 86 assert_response 401
83 87 assert_equal 'application/xml', response.content_type
84 88 end
89
90 test "PUT /projects/:project_id/wiki/:title.xml should update wiki page" do
91 assert_no_difference 'WikiPage.count' do
92 assert_difference 'WikiContent::Version.count' do
93 put '/projects/ecookbook/wiki/CookBook_documentation.xml',
94 {:wiki_page => {:text => 'New content from API', :comments => 'API update'}},
95 credentials('jsmith')
96 assert_response 200
97 end
98 end
99
100 page = WikiPage.find(1)
101 assert_equal 'New content from API', page.content.text
102 assert_equal 4, page.content.version
103 assert_equal 'API update', page.content.comments
104 assert_equal 'jsmith', page.content.author.login
105 end
106
107 test "PUT /projects/:project_id/wiki/:title.xml with current versino should update wiki page" do
108 assert_no_difference 'WikiPage.count' do
109 assert_difference 'WikiContent::Version.count' do
110 put '/projects/ecookbook/wiki/CookBook_documentation.xml',
111 {:wiki_page => {:text => 'New content from API', :comments => 'API update', :version => '3'}},
112 credentials('jsmith')
113 assert_response 200
114 end
115 end
116
117 page = WikiPage.find(1)
118 assert_equal 'New content from API', page.content.text
119 assert_equal 4, page.content.version
120 assert_equal 'API update', page.content.comments
121 assert_equal 'jsmith', page.content.author.login
122 end
123
124 test "PUT /projects/:project_id/wiki/:title.xml with stale version should respond with 409" do
125 assert_no_difference 'WikiPage.count' do
126 assert_no_difference 'WikiContent::Version.count' do
127 put '/projects/ecookbook/wiki/CookBook_documentation.xml',
128 {:wiki_page => {:text => 'New content from API', :comments => 'API update', :version => '2'}},
129 credentials('jsmith')
130 assert_response 409
131 end
132 end
133 end
134
135 test "PUT /projects/:project_id/wiki/:title.xml should create the page if it does not exist" do
136 assert_difference 'WikiPage.count' do
137 assert_difference 'WikiContent::Version.count' do
138 put '/projects/ecookbook/wiki/New_page_from_API.xml',
139 {:wiki_page => {:text => 'New content from API', :comments => 'API create'}},
140 credentials('jsmith')
141 assert_response 201
142 end
143 end
144
145 page = WikiPage.order('id DESC').first
146 assert_equal 'New_page_from_API', page.title
147 assert_equal 'New content from API', page.content.text
148 assert_equal 1, page.content.version
149 assert_equal 'API create', page.content.comments
150 assert_equal 'jsmith', page.content.author.login
151 assert_nil page.parent
152 end
153
154 test "PUT /projects/:project_id/wiki/:title.xml with parent" do
155 assert_difference 'WikiPage.count' do
156 assert_difference 'WikiContent::Version.count' do
157 put '/projects/ecookbook/wiki/New_subpage_from_API.xml',
158 {:wiki_page => {:parent_title => 'CookBook_documentation', :text => 'New content from API', :comments => 'API create'}},
159 credentials('jsmith')
160 assert_response 201
161 end
162 end
163
164 page = WikiPage.order('id DESC').first
165 assert_equal 'New_subpage_from_API', page.title
166 assert_equal WikiPage.find(1), page.parent
167 end
85 168 end
@@ -1,159 +1,172
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../../test_helper', __FILE__)
19 19
20 20 class RoutingWikiTest < ActionController::IntegrationTest
21 21 def test_wiki_matching
22 22 assert_routing(
23 23 { :method => 'get', :path => "/projects/567/wiki" },
24 24 { :controller => 'wiki', :action => 'show', :project_id => '567' }
25 25 )
26 26 assert_routing(
27 27 { :method => 'get', :path => "/projects/567/wiki/lalala" },
28 28 { :controller => 'wiki', :action => 'show', :project_id => '567',
29 29 :id => 'lalala' }
30 30 )
31 31 assert_routing(
32 32 { :method => 'get', :path => "/projects/567/wiki/lalala.pdf" },
33 33 { :controller => 'wiki', :action => 'show', :project_id => '567',
34 34 :id => 'lalala', :format => 'pdf' }
35 35 )
36 36 assert_routing(
37 { :method => 'get', :path => "/projects/567/wiki/lalala.xml" },
38 { :controller => 'wiki', :action => 'show', :project_id => '567',
39 :id => 'lalala', :format => 'xml' }
40 )
41 assert_routing(
42 { :method => 'get', :path => "/projects/567/wiki/lalala.json" },
43 { :controller => 'wiki', :action => 'show', :project_id => '567',
44 :id => 'lalala', :format => 'json' }
45 )
46 assert_routing(
47 37 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/diff" },
48 38 { :controller => 'wiki', :action => 'diff', :project_id => '1',
49 39 :id => 'CookBook_documentation' }
50 40 )
51 41 assert_routing(
52 42 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2" },
53 43 { :controller => 'wiki', :action => 'show', :project_id => '1',
54 44 :id => 'CookBook_documentation', :version => '2' }
55 45 )
56 46 assert_routing(
57 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2.xml" },
58 { :controller => 'wiki', :action => 'show', :project_id => '1',
59 :id => 'CookBook_documentation', :version => '2', :format => 'xml' }
60 )
61 assert_routing(
62 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2.json" },
63 { :controller => 'wiki', :action => 'show', :project_id => '1',
64 :id => 'CookBook_documentation', :version => '2', :format => 'json' }
65 )
66 assert_routing(
67 47 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2/diff" },
68 48 { :controller => 'wiki', :action => 'diff', :project_id => '1',
69 49 :id => 'CookBook_documentation', :version => '2' }
70 50 )
71 51 assert_routing(
72 52 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2/annotate" },
73 53 { :controller => 'wiki', :action => 'annotate', :project_id => '1',
74 54 :id => 'CookBook_documentation', :version => '2' }
75 55 )
76 56 end
77 57
78 58 def test_wiki_misc
79 59 assert_routing(
80 60 { :method => 'get', :path => "/projects/567/wiki/date_index" },
81 61 { :controller => 'wiki', :action => 'date_index', :project_id => '567' }
82 62 )
83 63 assert_routing(
84 64 { :method => 'get', :path => "/projects/567/wiki/export" },
85 65 { :controller => 'wiki', :action => 'export', :project_id => '567' }
86 66 )
87 67 assert_routing(
88 68 { :method => 'get', :path => "/projects/567/wiki/export.pdf" },
89 69 { :controller => 'wiki', :action => 'export', :project_id => '567', :format => 'pdf' }
90 70 )
91 71 assert_routing(
92 72 { :method => 'get', :path => "/projects/567/wiki/index" },
93 73 { :controller => 'wiki', :action => 'index', :project_id => '567' }
94 74 )
95 assert_routing(
96 { :method => 'get', :path => "/projects/567/wiki/index.xml" },
97 { :controller => 'wiki', :action => 'index', :project_id => '567', :format => 'xml' }
98 )
99 assert_routing(
100 { :method => 'get', :path => "/projects/567/wiki/index.json" },
101 { :controller => 'wiki', :action => 'index', :project_id => '567', :format => 'json' }
102 )
103 75 end
104 76
105 77 def test_wiki_resources
106 78 assert_routing(
107 79 { :method => 'get', :path => "/projects/567/wiki/my_page/edit" },
108 80 { :controller => 'wiki', :action => 'edit', :project_id => '567',
109 81 :id => 'my_page' }
110 82 )
111 83 assert_routing(
112 84 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/history" },
113 85 { :controller => 'wiki', :action => 'history', :project_id => '1',
114 86 :id => 'CookBook_documentation' }
115 87 )
116 88 assert_routing(
117 89 { :method => 'get', :path => "/projects/22/wiki/ladida/rename" },
118 90 { :controller => 'wiki', :action => 'rename', :project_id => '22',
119 91 :id => 'ladida' }
120 92 )
121 93 ["post", "put"].each do |method|
122 94 assert_routing(
123 95 { :method => method, :path => "/projects/567/wiki/CookBook_documentation/preview" },
124 96 { :controller => 'wiki', :action => 'preview', :project_id => '567',
125 97 :id => 'CookBook_documentation' }
126 98 )
127 99 end
128 100 assert_routing(
129 101 { :method => 'post', :path => "/projects/22/wiki/ladida/rename" },
130 102 { :controller => 'wiki', :action => 'rename', :project_id => '22',
131 103 :id => 'ladida' }
132 104 )
133 105 assert_routing(
134 106 { :method => 'post', :path => "/projects/22/wiki/ladida/protect" },
135 107 { :controller => 'wiki', :action => 'protect', :project_id => '22',
136 108 :id => 'ladida' }
137 109 )
138 110 assert_routing(
139 111 { :method => 'post', :path => "/projects/22/wiki/ladida/add_attachment" },
140 112 { :controller => 'wiki', :action => 'add_attachment', :project_id => '22',
141 113 :id => 'ladida' }
142 114 )
143 115 assert_routing(
144 116 { :method => 'put', :path => "/projects/567/wiki/my_page" },
145 117 { :controller => 'wiki', :action => 'update', :project_id => '567',
146 118 :id => 'my_page' }
147 119 )
148 120 assert_routing(
149 121 { :method => 'delete', :path => "/projects/22/wiki/ladida" },
150 122 { :controller => 'wiki', :action => 'destroy', :project_id => '22',
151 123 :id => 'ladida' }
152 124 )
153 125 assert_routing(
154 126 { :method => 'delete', :path => "/projects/22/wiki/ladida/3" },
155 127 { :controller => 'wiki', :action => 'destroy_version', :project_id => '22',
156 128 :id => 'ladida', :version => '3' }
157 129 )
158 130 end
131
132 def test_api
133 assert_routing(
134 { :method => 'get', :path => "/projects/567/wiki/my_page.xml" },
135 { :controller => 'wiki', :action => 'show', :project_id => '567',
136 :id => 'my_page', :format => 'xml' }
137 )
138 assert_routing(
139 { :method => 'get', :path => "/projects/567/wiki/my_page.json" },
140 { :controller => 'wiki', :action => 'show', :project_id => '567',
141 :id => 'my_page', :format => 'json' }
142 )
143 assert_routing(
144 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2.xml" },
145 { :controller => 'wiki', :action => 'show', :project_id => '1',
146 :id => 'CookBook_documentation', :version => '2', :format => 'xml' }
147 )
148 assert_routing(
149 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2.json" },
150 { :controller => 'wiki', :action => 'show', :project_id => '1',
151 :id => 'CookBook_documentation', :version => '2', :format => 'json' }
152 )
153 assert_routing(
154 { :method => 'get', :path => "/projects/567/wiki/index.xml" },
155 { :controller => 'wiki', :action => 'index', :project_id => '567', :format => 'xml' }
156 )
157 assert_routing(
158 { :method => 'get', :path => "/projects/567/wiki/index.json" },
159 { :controller => 'wiki', :action => 'index', :project_id => '567', :format => 'json' }
160 )
161 assert_routing(
162 { :method => 'put', :path => "/projects/567/wiki/my_page.xml" },
163 { :controller => 'wiki', :action => 'update', :project_id => '567',
164 :id => 'my_page', :format => 'xml' }
165 )
166 assert_routing(
167 { :method => 'put', :path => "/projects/567/wiki/my_page.json" },
168 { :controller => 'wiki', :action => 'update', :project_id => '567',
169 :id => 'my_page', :format => 'json' }
170 )
171 end
159 172 end
General Comments 0
You need to be logged in to leave comments. Login now