Commit d71ef742d7548e843701028bfc0b79682afe68e6
1 parent
da1968e4
Exists in
staging
and in
29 other branches
api: move to app/api
Showing
59 changed files
with
1992 additions
and
2041 deletions
Show diff stats
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | ... | ... |
... | ... | @@ -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 | + | |