canvas-lms/app/controllers/wiki_pages_api_controller.rb

579 lines
21 KiB
Ruby

#
# Copyright (C) 2011 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
# @API Pages
#
# Pages are rich content associated with Courses and Groups in Canvas.
# The Pages API allows you to create, retrieve, update, and delete pages.
#
# @object Page
# {
# // the unique locator for the page
# "url": "my-page-title",
#
# // the title of the page
# "title": "My Page Title",
#
# // the creation date for the page
# "created_at": "2012-08-06T16:46:33-06:00",
#
# // the date the page was last updated
# "updated_at": "2012-08-08T14:25:20-06:00",
#
# // whether this page is hidden from students
# // (note: students will never see this true; pages hidden from them will be omitted from results)
# "hide_from_students": false,
#
# // roles allowed to edit the page; comma-separated list comprising a combination of
# // 'teachers', 'students', and/or 'public'
# // if not supplied, course defaults are used
# "editing_roles": "teachers,students",
#
# // the User who last edited the page
# // (this may not be present if the page was imported from another system)
# "last_edited_by": {
# "id": 133,
# "display_name": "Rey del Pueblo",
# "avatar_image_url": "https://canvas.example.com/images/thumbnails/bm90aGluZyBoZXJl",
# "html_url": "https://canvas.example.com/courses/789/users/133"
# },
#
# // the page content, in HTML
# // (present when requesting a single page; omitted when listing pages)
# "body": "<p>Page Content</p>",
#
# // whether the page is published
# "published": true,
#
# // whether this page is the front page for the wiki
# "front_page": false,
#
# // Whether or not this is locked for the user.
# "locked_for_user": false,
#
# // (Optional) Information for the user about the lock. Present when locked_for_user is true.
# "lock_info": {
# // Asset string for the object causing the lock
# "asset_string": "wiki_page_1",
#
# // (Optional) Context module causing the lock.
# "context_module": {}
# },
#
# // (Optional) An explanation of why this is locked for the user. Present when locked_for_user is true.
# "lock_explanation": "This page is locked until September 1 at 12:00am"
# }
#
# @object PageRevision
# {
# // an identifier for this revision of the page
# "revision_id": 7,
#
# // the time when this revision was saved
# "updated_at": "2012-08-07T11:23:58-06:00",
#
# // the User who saved this revision, if applicable
# // (this may not be present if the page was imported from another system)
# "edited_by": {
# "id": 1123,
# "display_name": "Leonardo Fibonacci",
# "avatar_image_url": "https://canvas.example.com/images/thumbnails/bWVhbmluZ2xlc3M=",
# "html_url": "https://canvas.example.com/courses/789/users/1123"
# },
#
# // the following fields are not included in the index action
# // and may be omitted from the show action via summary=1
#
# // the historic url of the page
# "url": "old-page-title",
#
# // the historic page title
# "title": "Old Page Title",
#
# // the historic page contents
# "body": "<p>Old Page Content</p>"
# }
class WikiPagesApiController < ApplicationController
before_filter :require_context
before_filter :get_wiki_page, :except => [:create, :index]
before_filter :was_front_page, :except => [:index]
include Api::V1::WikiPage
# @API List pages
#
# List the wiki pages associated with a course or group
#
# @argument sort [Optional, String, "title"|"created_at"|"updated_at"]
# Sort results by this field.
#
# @argument order [Optional, String, "asc"|"desc"]
# The sorting order. Defaults to 'asc'.
#
# @argument search_term [Optional, String]
# The partial title of the pages to match and return.
#
# @example_request
# curl -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/123/pages?sort=title&order=asc
#
# @returns [Page]
def index
if authorized_action(@context.wiki, @current_user, :read)
pages_route = polymorphic_url([:api_v1, @context, :wiki_pages])
# omit body from selection, since it's not included in index results
scope = @context.wiki.wiki_pages.select(WikiPage.column_names - ['body']).includes(:user)
scope = @context.grants_right?(@current_user, session, :view_unpublished_items) ? scope.not_deleted : scope.active
scope = scope.not_hidden unless @context.grants_right?(@current_user, session, :view_hidden_items)
scope = WikiPage.search_by_attribute(scope, :title, params[:search_term])
order_clause = case params[:sort]
when 'title'
WikiPage.title_order_by_clause
when 'created_at'
'wiki_pages.created_at'
when 'updated_at'
'wiki_pages.updated_at'
else
'wiki_pages.id'
end
order_clause += ' DESC' if params[:order] == 'desc'
scope = scope.order(order_clause)
wiki_pages = Api.paginate(scope, self, pages_route)
render :json => wiki_pages_json(wiki_pages, @current_user, session)
end
end
# @API Show page
#
# Retrieve the content of a wiki page
#
# @argument url [String]
# The unique identifier for a page.
#
# @example_request
# curl -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/123/pages/my-page-url
#
# @example_request
# curl -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/groups/456/front_page
#
# @returns Page
def show
if authorized_action(@page, @current_user, :read)
@page.increment_view_count(@current_user, @context)
log_asset_access(@page, "wiki", @wiki)
render :json => wiki_page_json(@page, @current_user, session)
end
end
# @API Create page
#
# Create a new wiki page
#
# @argument wiki_page[title] [String]
# The title for the new page.
#
# @argument wiki_page[body] [String]
# The content for the new page.
#
# @argument wiki_page[hide_from_students] [Boolean]
# Whether the page should be hidden from students.
#
# *Note:* when draft state is enabled, attempts to set +hide_from_students+
# will be ignored and the value returned will always be the inverse of the
# +published+ value.
#
# @argument wiki_page[editing_roles] [Optional, String, "teachers"|"students"|"members"|"public"]
# Which user roles are allowed to edit this page. Any combination
# of these roles is allowed (separated by commas).
#
# "teachers":: Allows editing by teachers in the course.
# "students":: Allows editing by students in the course.
# "members":: For group wikis, allows editing by members of the group.
# "public":: Allows editing by any user.
#
# @argument wiki_page[notify_of_update] [Boolean]
# Whether participants should be notified when this page changes.
#
# @argument wiki_page[published] [Optional, Boolean]
# Whether the page is published (true) or draft state (false).
#
# *Note:* when draft state is disabled, attempts to set +published+
# will be ignored and the value returned will always be true.
#
# @argument wiki_page[front_page] [Optional, Boolean]
# Set an unhidden page as the front page (if true)
#
# @example_request
# curl -X POST -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/123/pages?wiki_page[title]=New+page&wiki_page[body]=New+body+text
#
# @returns Page
def create
@page = @context.wiki.wiki_pages.build
@wiki = @context.wiki
if authorized_action(@page, @current_user, :create)
update_params = get_update_params(Set[:title, :body])
if !update_params.is_a?(Symbol) && @page.update_attributes(update_params) && process_front_page
log_asset_access(@page, "wiki", @wiki, 'participate')
render :json => wiki_page_json(@page, @current_user, session)
else
render :json => @page.errors.to_json, :status => update_params.is_a?(Symbol) ? update_params : :bad_request
end
end
end
# @API Update page
#
# Update the title or contents of a wiki page
#
# @argument url [String]
# The unique identifier for a page.
#
# @argument wiki_page[title] [String]
# The title for the new page. NOTE: changing a page's title will change its
# url. The updated url will be returned in the result.
#
# @argument wiki_page[body] [String]
# The content for the new page.
#
# @argument wiki_page[hide_from_students] [Boolean]
# Whether the page should be hidden from students.
#
# *Note:* when draft state is enabled, attempts to set +hide_from_students+
# will be ignored and the value returned will always be the inverse of the
# +published+ value.
#
# @argument wiki_page[editing_roles] [Optional, String, "teachers"|"students"|"members"|"public"]
# Which user roles are allowed to edit this page. Any combination
# of these roles is allowed (separated by commas).
#
# "teachers":: Allows editing by teachers in the course.
# "students":: Allows editing by students in the course.
# "members":: For group wikis, allows editing by members of the group.
# "public":: Allows editing by any user.
#
# @argument wiki_page[notify_of_update] [Boolean]
# Whether participants should be notified when this page changes.
#
# @argument wiki_page[published] [Optional, Boolean]
# Whether the page is published (true) or draft state (false).
#
# *Note:* when draft state is disabled, attempts to set +published+
# will be ignored and the value returned will always be true.
#
# @argument wiki_page[front_page] [Optional, Boolean]
# Set an unhidden page as the front page (if true)
#
# @example_request
# curl -X PUT -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/123/pages/the-page-url?wiki_page[body]=Updated+body+text
#
# @example_request
# curl -X PUT -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/123/front_page?wiki_page[body]=Updated+body+text
#
# @returns Page
def update
if authorized_action(@page, @current_user, [:update, :update_content])
update_params = get_update_params
if !update_params.is_a?(Symbol) && @page.update_attributes(update_params) && process_front_page
log_asset_access(@page, "wiki", @wiki, 'participate')
@page.context_module_action(@current_user, @context, :contributed)
render :json => wiki_page_json(@page, @current_user, session)
else
render :json => @page.errors.to_json, :status => update_params.is_a?(Symbol) ? update_params : :bad_request
end
end
end
# @API Delete page
#
# Delete a wiki page
#
# @argument url [String]
# the unique identifier for a page.
#
# @example_request
# curl -X DELETE -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/123/pages/the-page-url
#
# @example_request
# curl -X DELETE -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/123/front_page
#
# @returns Page
def destroy
if authorized_action(@page, @current_user, :delete)
if !@was_front_page
@page.workflow_state = 'deleted'
@page.save!
process_front_page
render :json => wiki_page_json(@page, @current_user, session)
else
@page.errors.add(:front_page, t(:cannot_delete_front_page, 'The front page cannot be deleted'))
render :json => @page.errors.to_json, :status => :bad_request
end
end
end
# @API List revisions
#
# List the revisions of a page. Callers must have update rights on the page in order to see page history.
#
# @argument url [String]
# The unique identifier for a page
#
# @example_request
# curl -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/123/pages/the-page-url/revisions
#
# @returns [PageRevision]
def revisions
if authorized_action(@page, @current_user, :read_revisions)
route = polymorphic_url([:api_v1, @context, @page, :revisions])
scope = @page.versions
revisions = Api.paginate(scope, self, route)
render :json => wiki_page_revisions_json(revisions, @current_user, session)
end
end
# @API Show revision
#
# Retrieve the metadata and optionally content of a revision of the page.
# Note that retrieving historic versions of pages requires edit rights.
#
# @argument url [String]
# The unique identifier for a page
#
# @argument summary [Optional, Boolean]
# If set, exclude page content from results
#
# @example_request
# curl -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/123/pages/the-page-url/revisions/latest
#
# @example_request
# curl -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/123/pages/the-page-url/revisions/4
#
# @returns PageRevision
def show_revision
if params.has_key?(:revision_id)
permission = :read_revisions
revision = @page.versions.find_by_number!(params[:revision_id].to_i)
else
permission = :read
revision = @page.versions.current
end
if authorized_action(@page, @current_user, permission)
include_content = if params.has_key?(:summary)
!value_to_boolean(params[:summary])
else
true
end
render :json => wiki_page_revision_json(revision, @current_user, session, include_content)
end
end
# @API Revert to revision
#
# Revert a page to a prior revision.
#
# @argument url [String]
# The unique identifier for the page
#
# @argument revision_id [Integer]
# The revision to revert to (use the
# {api:WikiPagesApiController#revisions List Revisions API} to see
# available revisions)
#
# @example_request
# curl -X POST -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/123/pages/the-page-url/revisions/6
#
# @returns PageRevision
def revert
if authorized_action(@page, @current_user, :read_revisions) && authorized_action(@page, @current_user, :update)
revision_id = params[:revision_id].to_i
@revision = @page.versions.find_by_number!(revision_id).model
@page.body = @revision.body
@page.title = @revision.title
@page.url = @revision.url
@page.user_id = @current_user.id if @current_user
if @page.save
render :json => wiki_page_revision_json(@page.versions.current, @current_user, session, true)
else
render :json => @page.errors.to_json, :status => :bad_request
end
end
end
protected
def get_wiki_page
@wiki = @context.wiki
@wiki.check_has_front_page
url = params[:url]
if url.blank?
if @wiki.has_front_page?
url = @wiki.get_front_page_url
else
render :status => 404, :json => { :message => t(:no_wiki_front_page, "No front page has been set") }
return false
end
end
@page = @wiki.wiki_pages.not_deleted.find_by_url!(url)
end
def was_front_page
@was_front_page = false
@was_front_page = @page.is_front_page? if @page
end
def get_update_params(allowed_fields=Set[])
initialize_wiki_page
# normalize parameters
page_params = params[:wiki_page] || {}
if @context.draft_state_enabled?
page_params.slice!(*%w(title body notify_of_update published front_page editing_roles))
else
page_params.slice!(*%w(title body hide_from_students notify_of_update front_page editing_roles))
end
if page_params.has_key?(:published)
workflow_state = value_to_boolean(page_params.delete(:published)) ? 'active' : 'unpublished'
end
if page_params.has_key?(:editing_roles)
editing_roles = page_params[:editing_roles].split(',').map(&:strip)
invalid_roles = editing_roles.reject{|role| %w(teachers students members public).include?(role)}
unless invalid_roles.empty?
@page.errors.add(:editing_roles, t(:invalid_editing_roles, 'The provided editing roles are invalid'))
return :bad_request
end
page_params[:editing_roles] = editing_roles.join(',')
end
if page_params.has_key?(:front_page)
@set_front_page = true
@set_as_front_page = value_to_boolean(page_params.delete(:front_page))
end
change_front_page = @set_front_page && @was_front_page != @set_as_front_page
# check user permissions
rejected_fields = Set[]
if @wiki.grants_right?(@current_user, session, :manage)
allowed_fields.clear
else
rejected_fields << :published if workflow_state && workflow_state != @page.workflow_state
if editing_roles
existing_editing_roles = (@page.editing_roles || '').split(',')
editing_roles_changed = existing_editing_roles.reject{|role| editing_roles.include?(role)}.length > 0
editing_roles_changed |= editing_roles.reject{|role| existing_editing_roles.include?(role)}.length > 0
rejected_fields << :editing_roles if editing_roles_changed
end
rejected_fields << :hide_from_students if page_params.include?(:hide_from_students) && value_to_boolean(page_params[:hide_from_students]) != @page.hide_from_students
unless @page.grants_right?(@current_user, session, :update)
allowed_fields << :body
rejected_fields << :title if page_params.include?(:title) && page_params[:title] != @page.title
rejected_fields << :front_page if change_front_page && !@wiki.grants_right?(@current_user, session, :update)
end
end
# check rejected fields
rejected_fields = rejected_fields - allowed_fields
unless rejected_fields.empty?
@page.errors.add(:published, t(:cannot_update_published, 'You are not allowed to update the published state of this wiki page')) if rejected_fields.include?(:published)
@page.errors.add(:title, t(:cannot_update_title, 'You are not allowed to update the title of this wiki page')) if rejected_fields.include?(:title)
@page.errors.add(:hide_from_students, t(:cannot_update_hide_from_students, 'You are not allowed to update the hidden from students flag of this wiki page')) if rejected_fields.include?(:hide_from_students)
@page.errors.add(:editing_roles, t(:cannot_update_editing_roles, 'You are not allowed to update the editing roles of this wiki page')) if rejected_fields.include?(:editing_roles)
@page.errors.add(:front_page, t(:cannot_update_front_page, 'You are not allowed to change the wiki front page')) if rejected_fields.include?(:front_page)
return :unauthorized
end
# check for a valid front page
valid_front_page = true
if change_front_page || page_params.include?(:hide_from_students)
new_front_page = change_front_page ? @set_as_front_page : @page.is_front_page?
new_hide_from_students = page_params.include?(:hide_from_students) ? value_to_boolean(page_params[:hide_from_students]) : @page.hide_from_students
if new_front_page && new_hide_from_students
valid_front_page = false
error_message = t(:cannot_hide_front_page, 'The front page cannot be hidden from students')
@page.errors.add(:front_page, error_message) if change_front_page
@page.errors.add(:hide_from_students, error_message) if page_params.include?(:hide_from_students)
end
end
if change_front_page || workflow_state
new_front_page = change_front_page ? @set_as_front_page : @page.is_front_page?
new_workflow_state = workflow_state ? workflow_state : @page.workflow_state
valid_front_page = false if new_front_page && new_workflow_state != 'active'
if new_front_page && new_workflow_state != 'active'
valid_front_page = false
error_message = t(:cannot_have_unpublished_front_page, 'The front page cannot be unpublished')
@page.errors.add(:front_page, error_message) if change_front_page
@page.errors.add(:published, error_message) if workflow_state
end
end
return :bad_request unless valid_front_page
# limit to just the allowed fields
unless allowed_fields.empty?
page_params.slice!(*allowed_fields.to_a)
end
@page.workflow_state = workflow_state if workflow_state
page_params[:user_id] = @current_user.id if @current_user
page_params
end
def process_front_page
if @set_front_page
if @set_as_front_page && !@page.is_front_page?
return @page.set_as_front_page!
elsif !@set_as_front_page
return @page.wiki.unset_front_page!
end
elsif @was_front_page
if @page.deleted?
return @page.wiki.unset_front_page!
elsif !@page.is_front_page?
# if url changes, keep as front page
return @page.set_as_front_page!
end
end
return true
end
end