Commit d71ef742d7548e843701028bfc0b79682afe68e6

Authored by Braulio Bhavamitra
1 parent da1968e4

api: move to app/api

Showing 59 changed files with 1992 additions and 2041 deletions   Show diff stats
app/api/app.rb 0 → 100644
... ... @@ -0,0 +1,89 @@
  1 +require_dependency 'api/helpers'
  2 +
  3 +module Api
  4 + class App < Grape::API
  5 + use Rack::JSONP
  6 +
  7 + logger = Logger.new(File.join(Rails.root, 'log', "#{ENV['RAILS_ENV'] || 'production'}_api.log"))
  8 + logger.formatter = GrapeLogging::Formatters::Default.new
  9 + #use GrapeLogging::Middleware::RequestLogger, { logger: logger }
  10 +
  11 + rescue_from :all do |e|
  12 + logger.error e
  13 + error! e.message, 500
  14 + end
  15 +
  16 + @@NOOSFERO_CONF = nil
  17 + def self.NOOSFERO_CONF
  18 + if @@NOOSFERO_CONF
  19 + @@NOOSFERO_CONF
  20 + else
  21 + file = Rails.root.join('config', 'noosfero.yml')
  22 + @@NOOSFERO_CONF = File.exists?(file) ? YAML.load_file(file)[Rails.env] || {} : {}
  23 + end
  24 + end
  25 +
  26 + before { set_locale }
  27 + before { setup_multitenancy }
  28 + before { detect_stuff_by_domain }
  29 + before { filter_disabled_plugins_endpoints }
  30 + before { init_noosfero_plugins }
  31 + after { set_session_cookie }
  32 +
  33 + version 'v1'
  34 + prefix [ENV['RAILS_RELATIVE_URL_ROOT'], "api"].compact.join('/')
  35 + format :json
  36 + content_type :txt, "text/plain"
  37 +
  38 + helpers Helpers
  39 +
  40 + mount V1::Session
  41 + mount V1::Articles
  42 + mount V1::Comments
  43 + mount V1::Users
  44 + mount V1::Communities
  45 + mount V1::People
  46 + mount V1::Enterprises
  47 + mount V1::Categories
  48 + mount V1::Tasks
  49 + mount V1::Tags
  50 + mount V1::Environments
  51 + mount V1::Search
  52 + mount V1::Contacts
  53 + mount V1::Boxes
  54 + mount V1::Blocks
  55 + mount V1::Profiles
  56 + mount V1::Activities
  57 +
  58 + # hook point which allow plugins to add Grape::API extensions to Api::App
  59 + #finds for plugins which has api mount points classes defined (the class should extends Grape::API)
  60 + @plugins = Noosfero::Plugin.all.map { |p| p.constantize }
  61 + @plugins.each do |klass|
  62 + if klass.public_methods.include? :api_mount_points
  63 + klass.api_mount_points.each do |mount_class|
  64 + mount mount_class if mount_class && ( mount_class < Grape::API )
  65 + end
  66 + end
  67 + end
  68 +
  69 + def self.endpoint_unavailable?(endpoint, environment)
  70 + api_class = endpoint.options[:app] || endpoint.options[:for]
  71 + if api_class.present?
  72 + klass = api_class.name.deconstantize.constantize
  73 + return klass < Noosfero::Plugin && !environment.plugin_enabled?(klass)
  74 + end
  75 + end
  76 +
  77 + class << self
  78 + def endpoints_with_plugins(environment = nil)
  79 + if environment.present?
  80 + cloned_endpoints = endpoints_without_plugins.dup
  81 + cloned_endpoints.delete_if { |endpoint| endpoint_unavailable?(endpoint, environment) }
  82 + else
  83 + endpoints_without_plugins
  84 + end
  85 + end
  86 + alias_method_chain :endpoints, :plugins
  87 + end
  88 + end
  89 +end
... ...
app/api/entities.rb 0 → 100644
... ... @@ -0,0 +1,267 @@
  1 +module Api
  2 + module Entities
  3 +
  4 + Entity.format_with :timestamp do |date|
  5 + date.strftime('%Y/%m/%d %H:%M:%S') if date
  6 + end
  7 +
  8 + PERMISSIONS = {
  9 + :admin => 0,
  10 + :self => 10,
  11 + :private_content => 20,
  12 + :logged_user => 30,
  13 + :anonymous => 40
  14 + }
  15 +
  16 + def self.can_display_profile_field? profile, options, permission_options={}
  17 + permissions={:field => "", :permission => :private_content}
  18 + permissions.merge!(permission_options)
  19 + field = permissions[:field]
  20 + permission = permissions[:permission]
  21 + return true if profile.public? && profile.public_fields.map{|f| f.to_sym}.include?(field.to_sym)
  22 +
  23 + current_person = options[:current_person]
  24 +
  25 + current_permission = if current_person.present?
  26 + if current_person.is_admin?
  27 + :admin
  28 + elsif current_person == profile
  29 + :self
  30 + elsif profile.display_private_info_to?(current_person)
  31 + :private_content
  32 + else
  33 + :logged_user
  34 + end
  35 + else
  36 + :anonymous
  37 + end
  38 + PERMISSIONS[current_permission] <= PERMISSIONS[permission]
  39 + end
  40 +
  41 + class Image < Entity
  42 + root 'images', 'image'
  43 +
  44 + expose :url do |image, options|
  45 + image.public_filename
  46 + end
  47 +
  48 + expose :icon_url do |image, options|
  49 + image.public_filename(:icon)
  50 + end
  51 +
  52 + expose :minor_url do |image, options|
  53 + image.public_filename(:minor)
  54 + end
  55 +
  56 + expose :portrait_url do |image, options|
  57 + image.public_filename(:portrait)
  58 + end
  59 +
  60 + expose :thumb_url do |image, options|
  61 + image.public_filename(:thumb)
  62 + end
  63 + end
  64 +
  65 + class CategoryBase < Entity
  66 + root 'categories', 'category'
  67 + expose :name, :id, :slug
  68 + end
  69 +
  70 + class Category < CategoryBase
  71 + root 'categories', 'category'
  72 + expose :full_name do |category, options|
  73 + category.full_name
  74 + end
  75 + expose :parent, :using => CategoryBase, if: { parent: true }
  76 + expose :children, :using => CategoryBase, if: { children: true }
  77 + expose :image, :using => Image
  78 + expose :display_color
  79 + end
  80 +
  81 + class Region < Category
  82 + root 'regions', 'region'
  83 + expose :parent_id
  84 + end
  85 +
  86 + class Block < Entity
  87 + root 'blocks', 'block'
  88 + expose :id, :type, :settings, :position, :enabled
  89 + expose :mirror, :mirror_block_id, :title
  90 + expose :api_content, if: lambda { |object, options| options[:display_api_content] || object.display_api_content_by_default? }
  91 + end
  92 +
  93 + class Box < Entity
  94 + root 'boxes', 'box'
  95 + expose :id, :position
  96 + expose :blocks, :using => Block
  97 + end
  98 +
  99 + class Profile < Entity
  100 + expose :identifier, :name, :id
  101 + expose :created_at, :format_with => :timestamp
  102 + expose :updated_at, :format_with => :timestamp
  103 + expose :additional_data do |profile, options|
  104 + hash ={}
  105 + profile.public_values.each do |value|
  106 + hash[value.custom_field.name]=value.value
  107 + end
  108 +
  109 + private_values = profile.custom_field_values - profile.public_values
  110 + private_values.each do |value|
  111 + if Entities.can_display_profile_field?(profile,options)
  112 + hash[value.custom_field.name]=value.value
  113 + end
  114 + end
  115 + hash
  116 + end
  117 + expose :image, :using => Image
  118 + expose :region, :using => Region
  119 + expose :type
  120 + expose :custom_header
  121 + expose :custom_footer
  122 + end
  123 +
  124 + class UserBasic < Entity
  125 + expose :id
  126 + expose :login
  127 + end
  128 +
  129 + class Person < Profile
  130 + root 'people', 'person'
  131 + expose :user, :using => UserBasic, documentation: {type: 'User', desc: 'The user data of a person' }
  132 + expose :vote_count
  133 + expose :comments_count do |person, options|
  134 + person.comments.count
  135 + end
  136 + expose :following_articles_count do |person, options|
  137 + person.following_articles.count
  138 + end
  139 + expose :articles_count do |person, options|
  140 + person.articles.count
  141 + end
  142 + end
  143 +
  144 + class Enterprise < Profile
  145 + root 'enterprises', 'enterprise'
  146 + end
  147 +
  148 + class Community < Profile
  149 + root 'communities', 'community'
  150 + expose :description
  151 + expose :admins, :if => lambda { |community, options| community.display_info_to? options[:current_person]} do |community, options|
  152 + community.admins.map{|admin| {"name"=>admin.name, "id"=>admin.id, "username" => admin.identifier}}
  153 + end
  154 + expose :categories, :using => Category
  155 + expose :members, :using => Person , :if => lambda{ |community, options| community.display_info_to? options[:current_person] }
  156 + end
  157 +
  158 + class CommentBase < Entity
  159 + expose :body, :title, :id
  160 + expose :created_at, :format_with => :timestamp
  161 + expose :author, :using => Profile
  162 + expose :reply_of, :using => CommentBase
  163 + end
  164 +
  165 + class Comment < CommentBase
  166 + root 'comments', 'comment'
  167 + expose :children, as: :replies, :using => Comment
  168 + end
  169 +
  170 + class ArticleBase < Entity
  171 + root 'articles', 'article'
  172 + expose :id
  173 + expose :body
  174 + expose :abstract, documentation: {type: 'String', desc: 'Teaser of the body'}
  175 + expose :created_at, :format_with => :timestamp
  176 + expose :updated_at, :format_with => :timestamp
  177 + expose :title, :documentation => {:type => "String", :desc => "Title of the article"}
  178 + expose :created_by, :as => :author, :using => Profile, :documentation => {type: 'Profile', desc: 'The profile author that create the article'}
  179 + expose :profile, :using => Profile, :documentation => {type: 'Profile', desc: 'The profile associated with the article'}
  180 + expose :categories, :using => Category
  181 + expose :image, :using => Image
  182 + expose :votes_for
  183 + expose :votes_against
  184 + expose :setting
  185 + expose :position
  186 + expose :hits
  187 + expose :start_date
  188 + expose :end_date, :documentation => {type: 'DateTime', desc: 'The date of finish of the article'}
  189 + expose :tag_list
  190 + expose :children_count
  191 + expose :slug, :documentation => {:type => "String", :desc => "Trimmed and parsed name of a article"}
  192 + expose :path
  193 + expose :followers_count
  194 + expose :votes_count
  195 + expose :comments_count
  196 + expose :archived, :documentation => {:type => "Boolean", :desc => "Defines if a article is readonly"}
  197 + expose :type
  198 + expose :comments, using: CommentBase, :if => lambda{|obj,opt| opt[:params] && ['1','true',true].include?(opt[:params][:show_comments])}
  199 + expose :published
  200 + expose :accept_comments?, as: :accept_comments
  201 + end
  202 +
  203 + class Article < ArticleBase
  204 + root 'articles', 'article'
  205 + expose :parent, :using => ArticleBase
  206 + expose :children, :using => ArticleBase do |article, options|
  207 + article.children.published.limit(V1::Articles::MAX_PER_PAGE)
  208 + end
  209 + end
  210 +
  211 + class User < Entity
  212 + root 'users', 'user'
  213 +
  214 + attrs = [:id,:login,:email,:activated?]
  215 + aliases = {:activated? => :activated}
  216 +
  217 + attrs.each do |attribute|
  218 + name = aliases.has_key?(attribute) ? aliases[attribute] : attribute
  219 + expose attribute, :as => name, :if => lambda{|user,options| Entities.can_display_profile_field?(user.person, options, {:field => attribute})}
  220 + end
  221 +
  222 + expose :person, :using => Person, :if => lambda{|user,options| user.person.display_info_to? options[:current_person]}
  223 + expose :permissions, :if => lambda{|user,options| Entities.can_display_profile_field?(user.person, options, {:field => :permissions, :permission => :self})} do |user, options|
  224 + output = {}
  225 + user.person.role_assignments.map do |role_assigment|
  226 + if role_assigment.resource.respond_to?(:identifier) && !role_assigment.role.nil?
  227 + output[role_assigment.resource.identifier] = role_assigment.role.permissions
  228 + end
  229 + end
  230 + output
  231 + end
  232 + end
  233 +
  234 + class UserLogin < User
  235 + root 'users', 'user'
  236 + expose :private_token, documentation: {type: 'String', desc: 'A valid authentication code for post/delete api actions'}, if: lambda {|object, options| object.activated? }
  237 + end
  238 +
  239 + class Task < Entity
  240 + root 'tasks', 'task'
  241 + expose :id
  242 + expose :type
  243 + end
  244 +
  245 + class Environment < Entity
  246 + expose :name
  247 + expose :id
  248 + expose :description
  249 + expose :settings, if: lambda { |instance, options| options[:is_admin] }
  250 + end
  251 +
  252 + class Tag < Entity
  253 + root 'tags', 'tag'
  254 + expose :name
  255 + end
  256 +
  257 + class Activity < Entity
  258 + root 'activities', 'activity'
  259 + expose :id, :params, :verb, :created_at, :updated_at, :comments_count, :visible
  260 + expose :user, :using => Profile
  261 + expose :target do |activity, opts|
  262 + type_map = {Profile => ::Profile, ArticleBase => ::Article}.find {|h| activity.target.kind_of?(h.last)}
  263 + type_map.first.represent(activity.target) unless type_map.nil?
  264 + end
  265 + end
  266 + end
  267 +end
... ...
app/api/entity.rb 0 → 100644
... ... @@ -0,0 +1,27 @@
  1 +module Api
  2 + class Entity < Grape::Entity
  3 +
  4 + def initialize(object, options = {})
  5 + object = nil if object.is_a? Exception
  6 + super object, options
  7 + end
  8 +
  9 + def self.represent(objects, options = {})
  10 + if options[:has_exception]
  11 + data = super objects, options.merge(is_inner_data: true)
  12 + if objects.is_a? Exception
  13 + data.merge ok: false, error: {
  14 + type: objects.class.name,
  15 + message: objects.message
  16 + }
  17 + else
  18 + data = data.serializable_hash if data.is_a? Entity
  19 + data.merge ok: true, error: { type: 'Success', message: '' }
  20 + end
  21 + else
  22 + super objects, options
  23 + end
  24 + end
  25 +
  26 + end
  27 +end
... ...
app/api/helpers.rb 0 → 100644
... ... @@ -0,0 +1,421 @@
  1 +require 'base64'
  2 +require 'tempfile'
  3 +
  4 +module Api
  5 + module Helpers
  6 + PRIVATE_TOKEN_PARAM = :private_token
  7 + DEFAULT_ALLOWED_PARAMETERS = [:parent_id, :from, :until, :content_type, :author_id, :identifier, :archived]
  8 +
  9 + include SanitizeParams
  10 + include Noosfero::Plugin::HotSpot
  11 + include ForgotPasswordHelper
  12 + include SearchTermHelper
  13 +
  14 + def set_locale
  15 + I18n.locale = (params[:lang] || request.env['HTTP_ACCEPT_LANGUAGE'] || 'en')
  16 + end
  17 +
  18 + def init_noosfero_plugins
  19 + plugins
  20 + end
  21 +
  22 + def current_user
  23 + private_token = (params[PRIVATE_TOKEN_PARAM] || headers['Private-Token']).to_s
  24 + @current_user ||= User.find_by private_token: private_token
  25 + @current_user ||= plugins.dispatch("api_custom_login", request).first
  26 + @current_user
  27 + end
  28 +
  29 + def current_person
  30 + current_user.person unless current_user.nil?
  31 + end
  32 +
  33 + def is_admin?(environment)
  34 + return false unless current_user
  35 + return current_person.is_admin?(environment)
  36 + end
  37 +
  38 + def logout
  39 + @current_user = nil
  40 + end
  41 +
  42 + def environment
  43 + @environment
  44 + end
  45 +
  46 + def present_partial(model, options)
  47 + if(params[:fields].present?)
  48 + begin
  49 + fields = JSON.parse(params[:fields])
  50 + if fields.present?
  51 + options.merge!(fields.symbolize_keys.slice(:only, :except))
  52 + end
  53 + rescue
  54 + fields = params[:fields]
  55 + fields = fields.split(',') if fields.kind_of?(String)
  56 + options[:only] = Array.wrap(fields)
  57 + end
  58 + end
  59 + present model, options
  60 + end
  61 +
  62 + include FindByContents
  63 +
  64 + ####################################################################
  65 + #### SEARCH
  66 + ####################################################################
  67 + def multiple_search?(searches=nil)
  68 + ['index', 'category_index'].include?(params[:action]) || (searches && searches.size > 1)
  69 + end
  70 + ####################################################################
  71 +
  72 + def logger
  73 + logger = Logger.new(File.join(Rails.root, 'log', "#{ENV['RAILS_ENV'] || 'production'}_api.log"))
  74 + logger.formatter = GrapeLogging::Formatters::Default.new
  75 + logger
  76 + end
  77 +
  78 + def limit
  79 + limit = params[:limit].to_i
  80 + limit = default_limit if limit <= 0
  81 + limit
  82 + end
  83 +
  84 + def period(from_date, until_date)
  85 + return nil if from_date.nil? && until_date.nil?
  86 +
  87 + begin_period = from_date.nil? ? Time.at(0).to_datetime : from_date
  88 + end_period = until_date.nil? ? DateTime.now : until_date
  89 +
  90 + begin_period..end_period
  91 + end
  92 +
  93 + def parse_content_type(content_type)
  94 + return nil if content_type.blank?
  95 + content_type.split(',').map do |content_type|
  96 + content_type.camelcase
  97 + end
  98 + end
  99 +
  100 + def find_article(articles, id)
  101 + article = articles.find(id)
  102 + article.display_to?(current_person) ? article : forbidden!
  103 + end
  104 +
  105 + def post_article(asset, params)
  106 + return forbidden! unless current_person.can_post_content?(asset)
  107 +
  108 + klass_type = params[:content_type] || params[:article].delete(:type) || TinyMceArticle.name
  109 + return forbidden! unless klass_type.constantize <= Article
  110 +
  111 + article = klass_type.constantize.new(params[:article])
  112 + article.last_changed_by = current_person
  113 + article.created_by= current_person
  114 + article.profile = asset
  115 +
  116 + if !article.save
  117 + render_api_errors!(article.errors.full_messages)
  118 + end
  119 + present_partial article, :with => Entities::Article
  120 + end
  121 +
  122 + def present_article(asset)
  123 + article = find_article(asset.articles, params[:id])
  124 + present_partial article, :with => Entities::Article, :params => params
  125 + end
  126 +
  127 + def present_articles_for_asset(asset, method = 'articles')
  128 + articles = find_articles(asset, method)
  129 + present_articles(articles)
  130 + end
  131 +
  132 + def present_articles(articles)
  133 + present_partial paginate(articles), :with => Entities::Article, :params => params
  134 + end
  135 +
  136 + def find_articles(asset, method = 'articles')
  137 + articles = select_filtered_collection_of(asset, method, params)
  138 + if current_person.present?
  139 + articles = articles.display_filter(current_person, nil)
  140 + else
  141 + articles = articles.published
  142 + end
  143 + articles
  144 + end
  145 +
  146 + def find_task(asset, id)
  147 + task = asset.tasks.find(id)
  148 + current_person.has_permission?(task.permission, asset) ? task : forbidden!
  149 + end
  150 +
  151 + def post_task(asset, params)
  152 + klass_type= params[:content_type].nil? ? 'Task' : params[:content_type]
  153 + return forbidden! unless klass_type.constantize <= Task
  154 +
  155 + task = klass_type.constantize.new(params[:task])
  156 + task.requestor_id = current_person.id
  157 + task.target_id = asset.id
  158 + task.target_type = 'Profile'
  159 +
  160 + if !task.save
  161 + render_api_errors!(task.errors.full_messages)
  162 + end
  163 + present_partial task, :with => Entities::Task
  164 + end
  165 +
  166 + def present_task(asset)
  167 + task = find_task(asset, params[:id])
  168 + present_partial task, :with => Entities::Task
  169 + end
  170 +
  171 + def present_tasks(asset)
  172 + tasks = select_filtered_collection_of(asset, 'tasks', params)
  173 + tasks = tasks.select {|t| current_person.has_permission?(t.permission, asset)}
  174 + return forbidden! if tasks.empty? && !current_person.has_permission?(:perform_task, asset)
  175 + present_partial tasks, :with => Entities::Task
  176 + end
  177 +
  178 + def make_conditions_with_parameter(params = {})
  179 + parsed_params = parser_params(params)
  180 + conditions = {}
  181 + from_date = DateTime.parse(parsed_params.delete(:from)) if parsed_params[:from]
  182 + until_date = DateTime.parse(parsed_params.delete(:until)) if parsed_params[:until]
  183 +
  184 + conditions[:type] = parse_content_type(parsed_params.delete(:content_type)) unless parsed_params[:content_type].nil?
  185 +
  186 + conditions[:created_at] = period(from_date, until_date) if from_date || until_date
  187 + conditions.merge!(parsed_params)
  188 +
  189 + conditions
  190 + end
  191 +
  192 + # changing make_order_with_parameters to avoid sql injection
  193 + def make_order_with_parameters(object, method, params)
  194 + order = "created_at DESC"
  195 + unless params[:order].blank?
  196 + if params[:order].include? '\'' or params[:order].include? '"'
  197 + order = "created_at DESC"
  198 + elsif ['RANDOM()', 'RANDOM'].include? params[:order].upcase
  199 + order = 'RANDOM()'
  200 + else
  201 + field_name, direction = params[:order].split(' ')
  202 + assoc = object.class.reflect_on_association(method.to_sym)
  203 + if !field_name.blank? and assoc
  204 + if assoc.klass.attribute_names.include? field_name
  205 + if direction.present? and ['ASC','DESC'].include? direction.upcase
  206 + order = "#{field_name} #{direction.upcase}"
  207 + end
  208 + end
  209 + end
  210 + end
  211 + end
  212 + return order
  213 + end
  214 +
  215 + def make_timestamp_with_parameters_and_method(params, method)
  216 + timestamp = nil
  217 + if params[:timestamp]
  218 + datetime = DateTime.parse(params[:timestamp])
  219 + table_name = method.to_s.singularize.camelize.constantize.table_name
  220 + timestamp = "#{table_name}.updated_at >= '#{datetime}'"
  221 + end
  222 +
  223 + timestamp
  224 + end
  225 +
  226 + def by_reference(scope, params)
  227 + reference_id = params[:reference_id].to_i == 0 ? nil : params[:reference_id].to_i
  228 + if reference_id.nil?
  229 + scope
  230 + else
  231 + created_at = scope.find(reference_id).created_at
  232 + scope.send("#{params.key?(:oldest) ? 'older_than' : 'younger_than'}", created_at)
  233 + end
  234 + end
  235 +
  236 + def by_categories(scope, params)
  237 + category_ids = params[:category_ids]
  238 + if category_ids.nil?
  239 + scope
  240 + else
  241 + scope.joins(:categories).where(:categories => {:id => category_ids})
  242 + end
  243 + end
  244 +
  245 + def select_filtered_collection_of(object, method, params)
  246 + conditions = make_conditions_with_parameter(params)
  247 + order = make_order_with_parameters(object,method,params)
  248 + timestamp = make_timestamp_with_parameters_and_method(params, method)
  249 +
  250 + objects = object.send(method)
  251 + objects = by_reference(objects, params)
  252 + objects = by_categories(objects, params)
  253 +
  254 + objects = objects.where(conditions).where(timestamp).reorder(order)
  255 +
  256 + params[:page] ||= 1
  257 + params[:per_page] ||= limit
  258 + paginate(objects)
  259 + end
  260 +
  261 + def authenticate!
  262 + unauthorized! unless current_user
  263 + end
  264 +
  265 + def profiles_for_person(profiles, person)
  266 + if person
  267 + profiles.listed_for_person(person)
  268 + else
  269 + profiles.visible
  270 + end
  271 + end
  272 +
  273 + # Checks the occurrences of uniqueness of attributes, each attribute must be present in the params hash
  274 + # or a Bad Request error is invoked.
  275 + #
  276 + # Parameters:
  277 + # keys (unique) - A hash consisting of keys that must be unique
  278 + def unique_attributes!(obj, keys)
  279 + keys.each do |key|
  280 + cant_be_saved_request!(key) if obj.find_by(key.to_s => params[key])
  281 + end
  282 + end
  283 +
  284 + def attributes_for_keys(keys)
  285 + attrs = {}
  286 + keys.each do |key|
  287 + attrs[key] = params[key] if params[key].present? or (params.has_key?(key) and params[key] == false)
  288 + end
  289 + attrs
  290 + end
  291 +
  292 + ##########################################
  293 + # error helpers #
  294 + ##########################################
  295 +
  296 + def not_found!
  297 + render_api_error!('404 Not found', 404)
  298 + end
  299 +
  300 + def forbidden!
  301 + render_api_error!('403 Forbidden', 403)
  302 + end
  303 +
  304 + def cant_be_saved_request!(attribute)
  305 + message = _("(Invalid request) %s can't be saved") % attribute
  306 + render_api_error!(message, 400)
  307 + end
  308 +
  309 + def bad_request!(attribute)
  310 + message = _("(Invalid request) %s not given") % attribute
  311 + render_api_error!(message, 400)
  312 + end
  313 +
  314 + def something_wrong!
  315 + message = _("Something wrong happened")
  316 + render_api_error!(message, 400)
  317 + end
  318 +
  319 + def unauthorized!
  320 + render_api_error!(_('Unauthorized'), 401)
  321 + end
  322 +
  323 + def not_allowed!
  324 + render_api_error!(_('Method Not Allowed'), 405)
  325 + end
  326 +
  327 + # javascript_console_message is supposed to be executed as console.log()
  328 + def render_api_error!(user_message, status, log_message = nil, javascript_console_message = nil)
  329 + message_hash = {'message' => user_message, :code => status}
  330 + message_hash[:javascript_console_message] = javascript_console_message if javascript_console_message.present?
  331 + log_msg = "#{status}, User message: #{user_message}"
  332 + log_msg = "#{log_message}, #{log_msg}" if log_message.present?
  333 + log_msg = "#{log_msg}, Javascript Console Message: #{javascript_console_message}" if javascript_console_message.present?
  334 + logger.error log_msg unless Rails.env.test?
  335 + error!(message_hash, status)
  336 + end
  337 +
  338 + def render_api_errors!(messages)
  339 + messages = messages.to_a if messages.class == ActiveModel::Errors
  340 + render_api_error!(messages.join(','), 400)
  341 + end
  342 +
  343 + protected
  344 +
  345 + def set_session_cookie
  346 + cookies['_noosfero_api_session'] = { value: @current_user.private_token, httponly: true } if @current_user.present?
  347 + end
  348 +
  349 + def setup_multitenancy
  350 + Noosfero::MultiTenancy.setup!(request.host)
  351 + end
  352 +
  353 + def detect_stuff_by_domain
  354 + @domain = Domain.by_name(request.host)
  355 + if @domain.nil?
  356 + @environment = Environment.default
  357 + if @environment.nil? && Rails.env.development?
  358 + # This should only happen in development ...
  359 + @environment = Environment.create!(:name => "Noosfero", :is_default => true)
  360 + end
  361 + else
  362 + @environment = @domain.environment
  363 + end
  364 + end
  365 +
  366 + def filter_disabled_plugins_endpoints
  367 + not_found! if Api::App.endpoint_unavailable?(self, @environment)
  368 + end
  369 +
  370 + def asset_with_image params
  371 + if params.has_key? :image_builder
  372 + asset_api_params = params
  373 + asset_api_params[:image_builder] = base64_to_uploadedfile(asset_api_params[:image_builder])
  374 + return asset_api_params
  375 + end
  376 + params
  377 + end
  378 +
  379 + def base64_to_uploadedfile(base64_image)
  380 + tempfile = base64_to_tempfile base64_image
  381 + converted_image = base64_image
  382 + converted_image[:tempfile] = tempfile
  383 + return {uploaded_data: ActionDispatch::Http::UploadedFile.new(converted_image)}
  384 + end
  385 +
  386 + def base64_to_tempfile base64_image
  387 + base64_img_str = base64_image[:tempfile]
  388 + decoded_base64_str = Base64.decode64(base64_img_str)
  389 + tempfile = Tempfile.new(base64_image[:filename])
  390 + tempfile.write(decoded_base64_str.encode("ascii-8bit").force_encoding("utf-8"))
  391 + tempfile.rewind
  392 + tempfile
  393 + end
  394 + private
  395 +
  396 + def parser_params(params)
  397 + parsed_params = {}
  398 + params.map do |k,v|
  399 + parsed_params[k.to_sym] = v if DEFAULT_ALLOWED_PARAMETERS.include?(k.to_sym)
  400 + end
  401 + parsed_params
  402 + end
  403 +
  404 + def default_limit
  405 + 20
  406 + end
  407 +
  408 + def parse_content_type(content_type)
  409 + return nil if content_type.blank?
  410 + content_type.split(',').map do |content_type|
  411 + content_type.camelcase
  412 + end
  413 + end
  414 +
  415 + def period(from_date, until_date)
  416 + begin_period = from_date.nil? ? Time.at(0).to_datetime : from_date
  417 + end_period = until_date.nil? ? DateTime.now : until_date
  418 + begin_period..end_period
  419 + end
  420 + end
  421 +end
... ...
app/api/v1/activities.rb 0 → 100644
... ... @@ -0,0 +1,20 @@
  1 +module Api
  2 + module V1
  3 + class Activities < Grape::API
  4 + before { authenticate! }
  5 +
  6 + resource :profiles do
  7 +
  8 + get ':id/activities' do
  9 + profile = Profile.find_by id: params[:id]
  10 +
  11 + not_found! if profile.blank? || profile.secret || !profile.visible
  12 + forbidden! if !profile.secret && profile.visible && !profile.display_private_info_to?(current_person)
  13 +
  14 + activities = profile.activities.map(&:activity)
  15 + present activities, :with => Entities::Activity, :current_person => current_person
  16 + end
  17 + end
  18 + end
  19 + end
  20 +end
... ...
app/api/v1/articles.rb 0 → 100644
... ... @@ -0,0 +1,303 @@
  1 +module Api
  2 + module V1
  3 + class Articles < Grape::API
  4 +
  5 + ARTICLE_TYPES = Article.descendants.map{|a| a.to_s}
  6 +
  7 + MAX_PER_PAGE = 50
  8 +
  9 + resource :articles do
  10 +
  11 + paginate max_per_page: MAX_PER_PAGE
  12 + # Collect articles
  13 + #
  14 + # Parameters:
  15 + # from - date where the search will begin. If nothing is passed the default date will be the date of the first article created
  16 + # oldest - Collect the oldest articles. If nothing is passed the newest articles are collected
  17 + # limit - amount of articles returned. The default value is 20
  18 + #
  19 + # Example Request:
  20 + # GET host/api/v1/articles?from=2013-04-04-14:41:43&until=2015-04-04-14:41:43&limit=10&private_token=e96fff37c2238fdab074d1dcea8e6317
  21 +
  22 + desc 'Return all articles of all kinds' do
  23 + detail 'Get all articles filtered by fields in query params'
  24 + params Entities::Article.documentation
  25 + success Entities::Article
  26 + failure [[403, 'Forbidden']]
  27 + named 'ArticlesList'
  28 + headers [
  29 + 'Per-Page' => {
  30 + description: 'Total number of records',
  31 + required: false
  32 + }
  33 + ]
  34 + end
  35 + get do
  36 + present_articles_for_asset(environment)
  37 + end
  38 +
  39 + desc "Return one article by id" do
  40 + detail 'Get only one article by id. If not found the "forbidden" http error is showed'
  41 + params Entities::Article.documentation
  42 + success Entities::Article
  43 + failure [[403, 'Forbidden']]
  44 + named 'ArticleById'
  45 + end
  46 + get ':id', requirements: {id: /[0-9]+/} do
  47 + present_article(environment)
  48 + end
  49 +
  50 + post ':id' do
  51 + article = environment.articles.find(params[:id])
  52 + return forbidden! unless article.allow_edit?(current_person)
  53 + article.update_attributes!(asset_with_image(params[:article]))
  54 + present_partial article, :with => Entities::Article
  55 + end
  56 +
  57 + desc 'Report a abuse and/or violent content in a article by id' do
  58 + detail 'Submit a abuse (in general, a content violation) report about a specific article'
  59 + params Entities::Article.documentation
  60 + failure [[400, 'Bad Request']]
  61 + named 'ArticleReportAbuse'
  62 + end
  63 + post ':id/report_abuse' do
  64 + article = find_article(environment.articles, params[:id])
  65 + profile = article.profile
  66 + begin
  67 + abuse_report = AbuseReport.new(:reason => params[:report_abuse])
  68 + if !params[:content_type].blank?
  69 + article = params[:content_type].constantize.find(params[:content_id])
  70 + abuse_report.content = article_reported_version(article)
  71 + end
  72 +
  73 + current_person.register_report(abuse_report, profile)
  74 +
  75 + if !params[:content_type].blank?
  76 + abuse_report = AbuseReport.find_by reporter_id: current_person.id, abuse_complaint_id: profile.opened_abuse_complaint.id
  77 + Delayed::Job.enqueue DownloadReportedImagesJob.new(abuse_report, article)
  78 + end
  79 +
  80 + {
  81 + :success => true,
  82 + :message => _('Your abuse report was registered. The administrators are reviewing your report.'),
  83 + }
  84 + rescue Exception => exception
  85 + #logger.error(exception.to_s)
  86 + render_api_error!(_('Your report couldn\'t be saved due to some problem. Please contact the administrator.'), 400)
  87 + end
  88 +
  89 + end
  90 +
  91 + desc "Returns the articles I voted" do
  92 + detail 'Get the Articles I make a vote'
  93 + failure [[403, 'Forbidden']]
  94 + named 'ArticleFollowers'
  95 + end
  96 + #FIXME refactor this method
  97 + get 'voted_by_me' do
  98 + present_articles(current_person.votes.where(:voteable_type => 'Article').collect(&:voteable))
  99 + end
  100 +
  101 + desc 'Perform a vote on a article by id' do
  102 + detail 'Vote on a specific article with values: 1 (if you like) or -1 (if not)'
  103 + params Entities::UserLogin.documentation
  104 + failure [[401,'Unauthorized']]
  105 + named 'ArticleVote'
  106 + end
  107 + post ':id/vote' do
  108 + authenticate!
  109 + value = (params[:value] || 1).to_i
  110 + # FIXME verify allowed values
  111 + render_api_error!('Vote value not allowed', 400) unless [-1, 1].include?(value)
  112 + article = find_article(environment.articles, params[:id])
  113 + begin
  114 + vote = Vote.new(:voteable => article, :voter => current_person, :vote => value)
  115 + {:vote => vote.save!}
  116 + rescue ActiveRecord::RecordInvalid => e
  117 + render_api_error!(e.message, 400)
  118 + end
  119 + end
  120 +
  121 + desc "Returns the total followers for the article" do
  122 + detail 'Get the followers of a specific article by id'
  123 + failure [[403, 'Forbidden']]
  124 + named 'ArticleFollowers'
  125 + end
  126 + get ':id/followers' do
  127 + article = find_article(environment.articles, params[:id])
  128 + total = article.person_followers.count
  129 + {:total_followers => total}
  130 + end
  131 +
  132 + desc "Return the articles followed by me"
  133 + get 'followed_by_me' do
  134 + present_articles_for_asset(current_person, 'following_articles')
  135 + end
  136 +
  137 + desc "Add a follower for the article" do
  138 + detail 'Add the current user identified by private token, like a follower of a article'
  139 + params Entities::UserLogin.documentation
  140 + failure [[401, 'Unauthorized']]
  141 + named 'ArticleFollow'
  142 + end
  143 + post ':id/follow' do
  144 + authenticate!
  145 + article = find_article(environment.articles, params[:id])
  146 + if article.article_followers.exists?(:person_id => current_person.id)
  147 + {:success => false, :already_follow => true}
  148 + else
  149 + article_follower = ArticleFollower.new
  150 + article_follower.article = article
  151 + article_follower.person = current_person
  152 + article_follower.save!
  153 + {:success => true}
  154 + end
  155 + end
  156 +
  157 + desc 'Return the children of a article identified by id' do
  158 + detail 'Get all children articles of a specific article'
  159 + params Entities::Article.documentation
  160 + failure [[403, 'Forbidden']]
  161 + named 'ArticleChildren'
  162 + end
  163 +
  164 + paginate per_page: MAX_PER_PAGE, max_per_page: MAX_PER_PAGE
  165 + get ':id/children' do
  166 + article = find_article(environment.articles, params[:id])
  167 +
  168 + #TODO make tests for this situation
  169 + votes_order = params.delete(:order) if params[:order]=='votes_score'
  170 + articles = select_filtered_collection_of(article, 'children', params)
  171 + articles = articles.display_filter(current_person, article.profile)
  172 +
  173 + #TODO make tests for this situation
  174 + if votes_order
  175 + articles = articles.joins('left join votes on articles.id=votes.voteable_id').group('articles.id').reorder('sum(coalesce(votes.vote, 0)) DESC')
  176 + end
  177 + Article.hit(articles)
  178 + present_articles(articles)
  179 + end
  180 +