From: Andy Allan Date: Wed, 16 Feb 2022 14:58:30 +0000 (+0000) Subject: Merge pull request #3440 from mmd-osm/relationmemberlimit X-Git-Tag: live~2527 X-Git-Url: https://git.openstreetmap.org/rails.git/commitdiff_plain/2fabc46421f9e908ef6902ea7c4890a0032af446?hp=2efd73c672dae8f6956024638b4b090961e74781 Merge pull request #3440 from mmd-osm/relationmemberlimit Introduce relation member limit --- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 474c80b4f..aaa1b5d4b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,8 +38,8 @@ bundle exec rails test:all You can view test coverage statistics by browsing the `coverage` directory. -The tests are automatically run on Pull Requests and other commits with the -results shown on [Travis CI](https://travis-ci.org/openstreetmap/openstreetmap-website). +The tests are automatically run on Pull Requests and other commits via github +actions. The results shown are within the PR display on github. ## Static Analysis @@ -79,14 +79,6 @@ database, and update the list of available keys manually. Adding or removing keys to this list is therefore discouraged, but contributions to the descriptive texts are welcome. -## Code Documentation - -To generate the HTML documentation of the API/rails code, run the command - -``` -rake doc:app -``` - ## Committing When you submit patches, the project maintainer has to read them and diff --git a/Gemfile b/Gemfile index 7bb5d7054..21dbbf421 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source "https://rubygems.org" # Require rails -gem "rails", "6.1.4.4" +gem "rails", "6.1.4.6" # Require json for multi_json gem "json" diff --git a/Gemfile.lock b/Gemfile.lock index c17aa9832..db231028e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,66 +3,66 @@ GEM specs: aasm (5.2.0) concurrent-ruby (~> 1.0) - actioncable (6.1.4.4) - actionpack (= 6.1.4.4) - activesupport (= 6.1.4.4) + actioncable (6.1.4.6) + actionpack (= 6.1.4.6) + activesupport (= 6.1.4.6) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.4.4) - actionpack (= 6.1.4.4) - activejob (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) + actionmailbox (6.1.4.6) + actionpack (= 6.1.4.6) + activejob (= 6.1.4.6) + activerecord (= 6.1.4.6) + activestorage (= 6.1.4.6) + activesupport (= 6.1.4.6) mail (>= 2.7.1) - actionmailer (6.1.4.4) - actionpack (= 6.1.4.4) - actionview (= 6.1.4.4) - activejob (= 6.1.4.4) - activesupport (= 6.1.4.4) + actionmailer (6.1.4.6) + actionpack (= 6.1.4.6) + actionview (= 6.1.4.6) + activejob (= 6.1.4.6) + activesupport (= 6.1.4.6) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.4.4) - actionview (= 6.1.4.4) - activesupport (= 6.1.4.4) + actionpack (6.1.4.6) + actionview (= 6.1.4.6) + activesupport (= 6.1.4.6) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) actionpack-page_caching (1.2.4) actionpack (>= 4.0.0) - actiontext (6.1.4.4) - actionpack (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) + actiontext (6.1.4.6) + actionpack (= 6.1.4.6) + activerecord (= 6.1.4.6) + activestorage (= 6.1.4.6) + activesupport (= 6.1.4.6) nokogiri (>= 1.8.5) - actionview (6.1.4.4) - activesupport (= 6.1.4.4) + actionview (6.1.4.6) + activesupport (= 6.1.4.6) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) active_record_union (1.3.0) activerecord (>= 4.0) - activejob (6.1.4.4) - activesupport (= 6.1.4.4) + activejob (6.1.4.6) + activesupport (= 6.1.4.6) globalid (>= 0.3.6) - activemodel (6.1.4.4) - activesupport (= 6.1.4.4) - activerecord (6.1.4.4) - activemodel (= 6.1.4.4) - activesupport (= 6.1.4.4) + activemodel (6.1.4.6) + activesupport (= 6.1.4.6) + activerecord (6.1.4.6) + activemodel (= 6.1.4.6) + activesupport (= 6.1.4.6) activerecord-import (1.3.0) activerecord (>= 4.2) - activestorage (6.1.4.4) - actionpack (= 6.1.4.4) - activejob (= 6.1.4.4) - activerecord (= 6.1.4.4) - activesupport (= 6.1.4.4) + activestorage (6.1.4.6) + actionpack (= 6.1.4.6) + activejob (= 6.1.4.6) + activerecord (= 6.1.4.6) + activesupport (= 6.1.4.6) marcel (~> 1.0.0) mini_mime (>= 1.1.0) - activesupport (6.1.4.4) + activesupport (6.1.4.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -70,8 +70,8 @@ GEM zeitwerk (~> 2.3) addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) - annotate (3.1.1) - activerecord (>= 3.2, < 7.0) + annotate (3.2.0) + activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) argon2 (2.1.1) ffi (~> 1.14) @@ -80,17 +80,17 @@ GEM autoprefixer-rails (10.4.2.0) execjs (~> 2) aws-eventstream (1.2.0) - aws-partitions (1.549.0) - aws-sdk-core (3.125.5) + aws-partitions (1.554.0) + aws-sdk-core (3.126.1) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.53.0) - aws-sdk-core (~> 3, >= 3.125.0) + aws-sdk-kms (1.54.0) + aws-sdk-core (~> 3, >= 3.126.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.111.3) - aws-sdk-core (~> 3, >= 3.125.0) + aws-sdk-s3 (1.112.0) + aws-sdk-core (~> 3, >= 3.126.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.4.0) @@ -109,7 +109,7 @@ GEM smart_properties binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) - bootsnap (1.10.2) + bootsnap (1.10.3) msgpack (~> 1.2) bootstrap (4.5.3) autoprefixer-rails (>= 9.1.0) @@ -118,14 +118,14 @@ GEM bootstrap_form (4.5.0) actionpack (>= 5.2) activemodel (>= 5.2) - brakeman (5.2.0) + brakeman (5.2.1) browser (5.3.1) builder (3.2.4) bzip2-ffi (1.1.0) ffi (~> 1.0) cancancan (3.3.0) - canonical-rails (0.2.13) - rails (>= 4.1, <= 7.0) + canonical-rails (0.2.14) + rails (>= 4.1, <= 7.1) capybara (3.36.0) addressable matrix @@ -140,14 +140,14 @@ GEM composite_primary_keys (13.0.3) activerecord (~> 6.1.0) concurrent-ruby (1.1.9) - config (3.1.1) + config (4.0.0) deep_merge (~> 1.2, >= 1.2.1) dry-validation (~> 1.0, >= 1.0.0) connection_pool (2.2.5) crack (0.4.5) rexml crass (1.0.6) - dalli (3.2.0) + dalli (3.2.1) debug_inspector (1.1.0) deep_merge (1.2.2) delayed_job (4.1.10) @@ -173,7 +173,7 @@ GEM dry-logic (1.2.0) concurrent-ruby (~> 1.0) dry-core (~> 0.5, >= 0.5) - dry-schema (1.8.0) + dry-schema (1.9.0) concurrent-ruby (~> 1.0) dry-configurable (~> 0.13, >= 0.13.0) dry-core (~> 0.5, >= 0.5) @@ -247,9 +247,9 @@ GEM html_tokenizer (0.0.7) htmlentities (4.3.4) http_accept_language (2.1.1) - i18n (1.8.11) + i18n (1.10.0) concurrent-ruby (~> 1.0) - i18n-js (3.9.0) + i18n-js (3.9.1) i18n (>= 0.6.6) image_optim (0.31.1) exifr (~> 1.2, >= 1.2.2) @@ -257,9 +257,9 @@ GEM image_size (>= 1.5, < 4) in_threads (~> 1.3) progress (~> 3.0, >= 3.0.1) - image_optim_rails (0.4.3) + image_optim_rails (0.5.0) image_optim (~> 0.24) - rails + railties sprockets image_processing (1.12.1) mini_magick (>= 4.9.5, < 5) @@ -269,7 +269,7 @@ GEM jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) - jmespath (1.5.0) + jmespath (1.6.0) jquery-rails (4.4.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) @@ -286,7 +286,7 @@ GEM logstasher (2.1.5) activesupport (>= 5.2) request_store - loofah (2.13.0) + loofah (2.14.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -299,7 +299,7 @@ GEM mini_mime (1.1.2) mini_portile2 (2.7.1) minitest (5.15.0) - msgpack (1.4.4) + msgpack (1.4.5) multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.1.1) @@ -345,7 +345,7 @@ GEM omniauth-openid (2.0.1) omniauth (>= 1.0, < 3.0) rack-openid (~> 1.4.0) - omniauth-rails_csrf_protection (1.0.0) + omniauth-rails_csrf_protection (1.0.1) actionpack (>= 4.2) omniauth (~> 2.0) omniauth-windowslive (0.0.12) @@ -355,11 +355,11 @@ GEM parallel (1.21.0) parser (3.1.0.0) ast (~> 2.4.1) - pg (1.3.0) + pg (1.3.2) popper_js (1.16.0) progress (3.6.0) public_suffix (4.0.6) - puma (5.5.2) + puma (5.6.2) nio4r (~> 2.0) quad_tile (1.0.1) r2 (0.2.7) @@ -370,25 +370,25 @@ GEM rack-openid (1.4.2) rack (>= 1.1.0) ruby-openid (>= 2.1.8) - rack-protection (2.1.0) + rack-protection (2.2.0) rack rack-test (1.1.0) rack (>= 1.0, < 3) rack-uri_sanitizer (0.0.2) - rails (6.1.4.4) - actioncable (= 6.1.4.4) - actionmailbox (= 6.1.4.4) - actionmailer (= 6.1.4.4) - actionpack (= 6.1.4.4) - actiontext (= 6.1.4.4) - actionview (= 6.1.4.4) - activejob (= 6.1.4.4) - activemodel (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) + rails (6.1.4.6) + actioncable (= 6.1.4.6) + actionmailbox (= 6.1.4.6) + actionmailer (= 6.1.4.6) + actionpack (= 6.1.4.6) + actiontext (= 6.1.4.6) + actionview (= 6.1.4.6) + activejob (= 6.1.4.6) + activemodel (= 6.1.4.6) + activerecord (= 6.1.4.6) + activestorage (= 6.1.4.6) + activesupport (= 6.1.4.6) bundler (>= 1.15.0) - railties (= 6.1.4.4) + railties (= 6.1.4.6) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -402,24 +402,24 @@ GEM rails-i18n (6.0.0) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 7) - railties (6.1.4.4) - actionpack (= 6.1.4.4) - activesupport (= 6.1.4.4) + railties (6.1.4.6) + actionpack (= 6.1.4.6) + activesupport (= 6.1.4.6) method_source rake (>= 0.13) thor (~> 1.0) rainbow (3.1.1) rake (13.0.6) - rb-fsevent (0.11.0) + rb-fsevent (0.11.1) rb-inotify (0.10.1) ffi (~> 1.0) - regexp_parser (2.2.0) + regexp_parser (2.2.1) request_store (1.5.1) rack (>= 1.4) rexml (3.2.5) rinku (2.0.6) rotp (6.2.0) - rubocop (1.25.0) + rubocop (1.25.1) parallel (~> 1.10) parser (>= 3.1.0.0) rainbow (>= 2.2.2, < 4.0) @@ -428,9 +428,9 @@ GEM rubocop-ast (>= 1.15.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.15.1) + rubocop-ast (1.15.2) parser (>= 3.0.1.1) - rubocop-minitest (0.17.0) + rubocop-minitest (0.17.2) rubocop (>= 0.90, < 2.0) rubocop-performance (1.13.2) rubocop (>= 1.7.0, < 2.0) @@ -468,7 +468,7 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov-lcov (0.8.0) - simplecov_json_formatter (0.1.3) + simplecov_json_formatter (0.1.4) smart_properties (1.17.0) sprockets (4.0.2) concurrent-ruby (~> 1.0) @@ -477,8 +477,8 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - strong_migrations (0.7.9) - activerecord (>= 5) + strong_migrations (0.8.0) + activerecord (>= 5.2) terser (1.1.8) execjs (>= 0.3.0, < 3) thor (1.2.1) @@ -498,7 +498,7 @@ GEM websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.5.3) + zeitwerk (2.5.4) PLATFORMS ruby @@ -568,7 +568,7 @@ DEPENDENCIES r2 (~> 0.2.7) rack-cors rack-uri_sanitizer - rails (= 6.1.4.4) + rails (= 6.1.4.6) rails-controller-testing rails-i18n (~> 6.0.0) rinku (>= 2.0.6) diff --git a/INSTALL.md b/INSTALL.md index 1faf4bf22..68b0d120e 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -90,7 +90,7 @@ Installing other dependencies: * Install Homebrew from https://brew.sh/ * Install the latest version of Ruby: `brew install ruby` -* Install other dependencies: `brew install imagemagick libxml2 gd yarn pngcrush optipng pngquant jhead jpegoptim gifsicle svgo` +* Install other dependencies: `brew install imagemagick libxml2 gd yarn pngcrush optipng pngquant jhead jpegoptim gifsicle svgo advancecomp` * Install Bundler: `gem install bundler` (you might need to `sudo gem install bundler` if you get an error about permissions - or see note below about [developer Ruby setup](#rbenv)) You will need to tell `bundler` that `libxml2` is installed in a Homebrew location. If it uses the system-installed one then you will get errors installing the `libxml-ruby` gem later on. @@ -102,8 +102,7 @@ bundle config build.libxml-ruby --with-xml2-config=/usr/local/opt/libxml2/bin/xm If you want to run the tests, you need `geckodriver` as well: ``` -brew tap homebrew/cask -brew cask install geckodriver +brew install geckodriver ``` Note that OS X does not have a /home directory by default, so if you are using the GPX functions, you will need to change the directories specified in config/application.yml. diff --git a/app/controllers/api/traces_controller.rb b/app/controllers/api/traces_controller.rb index 9894441ff..aa8a06000 100644 --- a/app/controllers/api/traces_controller.rb +++ b/app/controllers/api/traces_controller.rb @@ -54,6 +54,8 @@ module Api send_data(trace.xml_file.read, :filename => "#{trace.id}.xml", :type => request.format.to_s, :disposition => "attachment") elsif request.format == Mime[:gpx] send_data(trace.xml_file.read, :filename => "#{trace.id}.gpx", :type => request.format.to_s, :disposition => "attachment") + elsif trace.file.attached? + redirect_to rails_blob_path(trace.file, :disposition => "attachment") else send_file(trace.trace_name, :filename => "#{trace.id}#{trace.extension_name}", :type => trace.mime_type, :disposition => "attachment") end @@ -97,12 +99,6 @@ module Api # Sanitise the user's filename name = file.original_filename.gsub(/[^a-zA-Z0-9.]/, "_") - # Get a temporary filename... - filename = "/tmp/#{rand}" - - # ...and save the uploaded file to that location - File.binwrite(filename, file.read) - # Create the trace object, falsely marked as already # inserted to stop the import daemon trying to load it trace = Trace.new( @@ -110,40 +106,14 @@ module Api :tagstring => tags, :description => description, :visibility => visibility, - :inserted => true, + :inserted => false, :user => current_user, - :timestamp => Time.now.getutc + :timestamp => Time.now.getutc, + :file => file ) - if trace.valid? - Trace.transaction do - begin - # Save the trace object - trace.save! - - # Rename the temporary file to the final name - FileUtils.mv(filename, trace.trace_name) - rescue StandardError - # Remove the file as we have failed to update the database - FileUtils.rm_f(filename) - - # Pass the exception on - raise - end - - begin - # Clear the inserted flag to make the import daemon load the trace - trace.inserted = false - trace.save! - rescue StandardError - # Remove the file as we have failed to update the database - FileUtils.rm_f(trace.trace_name) - - # Pass the exception on - raise - end - end - end + # Save the trace object + trace.save! # Finally save the user's preferred privacy level if pref = current_user.preferences.where(:k => "gps.trace.visibility").first diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index bcb4c1617..e54fa4a5d 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -25,7 +25,7 @@ class ConfirmationsController < ApplicationController render_unknown_user token.user.display_name else user = token.user - user.status = "active" + user.activate user.email_valid = true flash[:notice] = gravatar_status_message(user) if gravatar_enable(user) user.save! diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 331575964..502b1357f 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -46,7 +46,7 @@ class PasswordsController < ApplicationController if params[:user] current_user.pass_crypt = params[:user][:pass_crypt] current_user.pass_crypt_confirmation = params[:user][:pass_crypt_confirmation] - current_user.status = "active" if current_user.status == "pending" + current_user.activate if current_user.may_activate? current_user.email_valid = true if current_user.save diff --git a/app/controllers/traces_controller.rb b/app/controllers/traces_controller.rb index 43977c3ce..a9dbc8539 100644 --- a/app/controllers/traces_controller.rb +++ b/app/controllers/traces_controller.rb @@ -99,12 +99,8 @@ class TracesController < ApplicationController logger.info(params[:trace][:gpx_file].class.name) if params[:trace][:gpx_file].respond_to?(:read) - begin - @trace = do_create(params[:trace][:gpx_file], params[:trace][:tagstring], - params[:trace][:description], params[:trace][:visibility]) - rescue StandardError => e - logger.debug e - end + @trace = do_create(params[:trace][:gpx_file], params[:trace][:tagstring], + params[:trace][:description], params[:trace][:visibility]) if @trace.id flash[:notice] = t ".trace_uploaded" @@ -141,6 +137,8 @@ class TracesController < ApplicationController send_data(trace.xml_file.read, :filename => "#{trace.id}.xml", :type => request.format.to_s, :disposition => "attachment") elsif request.format == Mime[:gpx] send_data(trace.xml_file.read, :filename => "#{trace.id}.gpx", :type => request.format.to_s, :disposition => "attachment") + elsif trace.file.attached? + redirect_to rails_blob_path(trace.file, :disposition => "attachment") else send_file(trace.trace_name, :filename => "#{trace.id}#{trace.extension_name}", :type => trace.mime_type, :disposition => "attachment") end @@ -217,8 +215,12 @@ class TracesController < ApplicationController if trace.visible? && trace.inserted? if trace.public? || (current_user && current_user == trace.user) - expires_in 7.days, :private => !trace.public?, :public => trace.public? - send_file(trace.large_picture_name, :filename => "#{trace.id}.gif", :type => "image/gif", :disposition => "inline") + if trace.icon.attached? + redirect_to rails_blob_path(trace.image, :disposition => "inline") + else + expires_in 7.days, :private => !trace.public?, :public => trace.public? + send_file(trace.large_picture_name, :filename => "#{trace.id}.gif", :type => "image/gif", :disposition => "inline") + end else head :forbidden end @@ -234,8 +236,12 @@ class TracesController < ApplicationController if trace.visible? && trace.inserted? if trace.public? || (current_user && current_user == trace.user) - expires_in 7.days, :private => !trace.public?, :public => trace.public? - send_file(trace.icon_picture_name, :filename => "#{trace.id}_icon.gif", :type => "image/gif", :disposition => "inline") + if trace.icon.attached? + redirect_to rails_blob_path(trace.icon, :disposition => "inline") + else + expires_in 7.days, :private => !trace.public?, :public => trace.public? + send_file(trace.icon_picture_name, :filename => "#{trace.id}_icon.gif", :type => "image/gif", :disposition => "inline") + end else head :forbidden end @@ -252,62 +258,29 @@ class TracesController < ApplicationController # Sanitise the user's filename name = file.original_filename.gsub(/[^a-zA-Z0-9.]/, "_") - # Get a temporary filename... - filename = "/tmp/#{rand}" - - # ...and save the uploaded file to that location - File.binwrite(filename, file.read) - - # Create the trace object, falsely marked as already - # inserted to stop the import daemon trying to load it + # Create the trace object trace = Trace.new( :name => name, :tagstring => tags, :description => description, :visibility => visibility, - :inserted => true, + :inserted => false, :user => current_user, - :timestamp => Time.now.getutc + :timestamp => Time.now.getutc, + :file => file ) - if trace.valid? - Trace.transaction do - begin - # Save the trace object - trace.save! - - # Rename the temporary file to the final name - FileUtils.mv(filename, trace.trace_name) - rescue StandardError - # Remove the file as we have failed to update the database - FileUtils.rm_f(filename) - - # Pass the exception on - raise - end - - begin - # Clear the inserted flag to make the import daemon load the trace - trace.inserted = false - trace.save! - rescue StandardError - # Remove the file as we have failed to update the database - FileUtils.rm_f(trace.trace_name) - - # Pass the exception on - raise - end + # Save the trace object + if trace.save + # Finally save the user's preferred privacy level + if pref = current_user.preferences.where(:k => "gps.trace.visibility").first + pref.v = visibility + pref.save + else + current_user.preferences.create(:k => "gps.trace.visibility", :v => visibility) end end - # Finally save the user's preferred privacy level - if pref = current_user.preferences.where(:k => "gps.trace.visibility").first - pref.v = visibility - pref.save - else - current_user.preferences.create(:k => "gps.trace.visibility", :v => visibility) - end - trace end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index f7a82c08c..39a191d84 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -164,8 +164,6 @@ class UsersController < ApplicationController Rails.logger.info "create: #{session[:referer]}" - current_user.status = "pending" - if current_user.auth_provider.present? && current_user.pass_crypt.empty? # We are creating an account with external authentication and # no password was specified so create a random one @@ -202,15 +200,19 @@ class UsersController < ApplicationController ## # sets a user's status def set_status - @user.status = params[:status] - @user.save + @user.activate! if params[:event] == "activate" + @user.confirm! if params[:event] == "confirm" + @user.unconfirm! if params[:event] == "unconfirm" + @user.hide! if params[:event] == "hide" + @user.unhide! if params[:event] == "unhide" + @user.unsuspend! if params[:event] == "unsuspend" redirect_to user_path(:display_name => params[:display_name]) end ## # destroy a user, marking them as deleted and removing personal data def destroy - @user.destroy + @user.soft_destroy! redirect_to user_path(:display_name => params[:display_name]) end diff --git a/app/models/trace.rb b/app/models/trace.rb index b3d87fc09..bdafdd9d5 100644 --- a/app/models/trace.rb +++ b/app/models/trace.rb @@ -39,6 +39,10 @@ class Trace < ApplicationRecord scope :visible_to_all, -> { where(:visibility => %w[public identifiable]) } scope :tagged, ->(t) { joins(:tags).where(:gpx_file_tags => { :tag => t }) } + has_one_attached :file, :service => Settings.trace_file_storage + has_one_attached :image, :service => Settings.trace_image_storage + has_one_attached :icon, :service => Settings.trace_icon_storage + validates :user, :presence => true, :associated => true validates :name, :presence => true, :length => 1..255, :characters => true validates :description, :presence => { :on => :create }, :length => 1..255, :characters => true @@ -46,6 +50,7 @@ class Trace < ApplicationRecord validates :visibility, :inclusion => %w[private public trackable identifiable] after_destroy :remove_files + after_save :set_filename def tagstring tags.collect(&:tag).join(", ") @@ -68,6 +73,18 @@ class Trace < ApplicationRecord end end + def file=(attachable) + case attachable + when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile + super(:io => attachable, + :filename => attachable.original_filename, + :content_type => content_type(attachable.path), + :identify => false) + else + super(attachable) + end + end + def public? visibility == "public" || visibility == "identifiable" end @@ -80,29 +97,27 @@ class Trace < ApplicationRecord visibility == "identifiable" end - def large_picture=(data) - f = File.new(large_picture_name, "wb") - f.syswrite(data) - f.close - end - - def icon_picture=(data) - f = File.new(icon_picture_name, "wb") - f.syswrite(data) - f.close - end - def large_picture - f = File.new(large_picture_name, "rb") - data = f.sysread(File.size(f.path)) - f.close + if image.attached? + data = image.blob.download + else + f = File.new(large_picture_name, "rb") + data = f.sysread(File.size(f.path)) + f.close + end + data end def icon_picture - f = File.new(icon_picture_name, "rb") - data = f.sysread(File.size(f.path)) - f.close + if icon.attached? + data = icon.blob.download + else + f = File.new(icon_picture_name, "rb") + data = f.sysread(File.size(f.path)) + f.close + end + data end @@ -119,46 +134,22 @@ class Trace < ApplicationRecord end def mime_type - filetype = Open3.capture2("/usr/bin/file", "-Lbz", trace_name).first.chomp - gzipped = filetype.include?("gzip compressed") - bzipped = filetype.include?("bzip2 compressed") - zipped = filetype.include?("Zip archive") - tarred = filetype.include?("tar archive") - - if gzipped - "application/x-gzip" - elsif bzipped - "application/x-bzip2" - elsif zipped - "application/x-zip" - elsif tarred - "application/x-tar" + if file.attached? + file.content_type else - "application/gpx+xml" + content_type(trace_name) end end def extension_name - filetype = Open3.capture2("/usr/bin/file", "-Lbz", trace_name).first.chomp - gzipped = filetype.include?("gzip compressed") - bzipped = filetype.include?("bzip2 compressed") - zipped = filetype.include?("Zip archive") - tarred = filetype.include?("tar archive") - - if tarred && gzipped - ".tar.gz" - elsif tarred && bzipped - ".tar.bz2" - elsif tarred - ".tar" - elsif gzipped - ".gpx.gz" - elsif bzipped - ".gpx.bz2" - elsif zipped - ".zip" - else - ".gpx" + case mime_type + when "application/x-tar+gzip" then ".tar.gz" + when "application/x-tar+x-bzip2" then ".tar.bz2" + when "application/x-tar" then ".tar" + when "application/zip" then ".zip" + when "application/gzip" then ".gpx.gz" + when "application/x-bzip2" then ".gpx.bz2" + else ".gpx" end end @@ -207,106 +198,156 @@ class Trace < ApplicationRecord end def xml_file - filetype = Open3.capture2("/usr/bin/file", "-Lbz", trace_name).first.chomp - gzipped = filetype.include?("gzip compressed") - bzipped = filetype.include?("bzip2 compressed") - zipped = filetype.include?("Zip archive") - tarred = filetype.include?("tar archive") - - if gzipped || bzipped || zipped || tarred - file = Tempfile.new("trace.#{id}") - - if tarred && gzipped - system("tar", "-zxOf", trace_name, :out => file.path) - elsif tarred && bzipped - system("tar", "-jxOf", trace_name, :out => file.path) - elsif tarred - system("tar", "-xOf", trace_name, :out => file.path) - elsif gzipped - system("gunzip", "-c", trace_name, :out => file.path) - elsif bzipped - system("bunzip2", "-c", trace_name, :out => file.path) - elsif zipped - system("unzip", "-p", trace_name, "-x", "__MACOSX/*", :out => file.path, :err => "/dev/null") + with_trace_file do |trace_name| + filetype = Open3.capture2("/usr/bin/file", "-Lbz", trace_name).first.chomp + gzipped = filetype.include?("gzip compressed") + bzipped = filetype.include?("bzip2 compressed") + zipped = filetype.include?("Zip archive") + tarred = filetype.include?("tar archive") + + if gzipped || bzipped || zipped || tarred + file = Tempfile.new("trace.#{id}") + + if tarred && gzipped + system("tar", "-zxOf", trace_name, :out => file.path) + elsif tarred && bzipped + system("tar", "-jxOf", trace_name, :out => file.path) + elsif tarred + system("tar", "-xOf", trace_name, :out => file.path) + elsif gzipped + system("gunzip", "-c", trace_name, :out => file.path) + elsif bzipped + system("bunzip2", "-c", trace_name, :out => file.path) + elsif zipped + system("unzip", "-p", trace_name, "-x", "__MACOSX/*", :out => file.path, :err => "/dev/null") + end + + file.unlink + else + file = File.open(trace_name) end - file.unlink - else - file = File.open(trace_name) + file end - - file end def import logger.info("GPX Import importing #{name} (#{id}) from #{user.email}") - gpx = GPX::File.new(trace_name) - - f_lat = 0 - f_lon = 0 - first = true - - # If there are any existing points for this trace then delete them - Tracepoint.where(:gpx_id => id).delete_all - - gpx.points.each_slice(1_000) do |points| - # Gather the trace points together for a bulk import - tracepoints = [] + with_trace_file do |trace_name| + gpx = GPX::File.new(trace_name) + + f_lat = 0 + f_lon = 0 + first = true + + # If there are any existing points for this trace then delete them + Tracepoint.where(:gpx_id => id).delete_all + + gpx.points.each_slice(1_000) do |points| + # Gather the trace points together for a bulk import + tracepoints = [] + + points.each do |point| + if first + f_lat = point.latitude + f_lon = point.longitude + first = false + end + + tp = Tracepoint.new + tp.lat = point.latitude + tp.lon = point.longitude + tp.altitude = point.altitude + tp.timestamp = point.timestamp + tp.gpx_id = id + tp.trackid = point.segment + tracepoints << tp + end - points.each do |point| - if first - f_lat = point.latitude - f_lon = point.longitude - first = false + # Run the before_save and before_create callbacks, and then import them in bulk with activerecord-import + tracepoints.each do |tp| + tp.run_callbacks(:save) { false } + tp.run_callbacks(:create) { false } end - tp = Tracepoint.new - tp.lat = point.latitude - tp.lon = point.longitude - tp.altitude = point.altitude - tp.timestamp = point.timestamp - tp.gpx_id = id - tp.trackid = point.segment - tracepoints << tp + Tracepoint.import!(tracepoints) end - # Run the before_save and before_create callbacks, and then import them in bulk with activerecord-import - tracepoints.each do |tp| - tp.run_callbacks(:save) { false } - tp.run_callbacks(:create) { false } + if gpx.actual_points.positive? + max_lat = Tracepoint.where(:gpx_id => id).maximum(:latitude) + min_lat = Tracepoint.where(:gpx_id => id).minimum(:latitude) + max_lon = Tracepoint.where(:gpx_id => id).maximum(:longitude) + min_lon = Tracepoint.where(:gpx_id => id).minimum(:longitude) + + max_lat = max_lat.to_f / 10000000 + min_lat = min_lat.to_f / 10000000 + max_lon = max_lon.to_f / 10000000 + min_lon = min_lon.to_f / 10000000 + + self.latitude = f_lat + self.longitude = f_lon + image.attach(:io => gpx.picture(min_lat, min_lon, max_lat, max_lon, gpx.actual_points), :filename => "#{id}.gif", :content_type => "image/gif") + icon.attach(:io => gpx.icon(min_lat, min_lon, max_lat, max_lon), :filename => "#{id}_icon.gif", :content_type => "image/gif") + self.size = gpx.actual_points + self.inserted = true + save! end - Tracepoint.import!(tracepoints) + logger.info "done trace #{id}" + + gpx end + end - if gpx.actual_points.positive? - max_lat = Tracepoint.where(:gpx_id => id).maximum(:latitude) - min_lat = Tracepoint.where(:gpx_id => id).minimum(:latitude) - max_lon = Tracepoint.where(:gpx_id => id).maximum(:longitude) - min_lon = Tracepoint.where(:gpx_id => id).minimum(:longitude) - - max_lat = max_lat.to_f / 10000000 - min_lat = min_lat.to_f / 10000000 - max_lon = max_lon.to_f / 10000000 - min_lon = min_lon.to_f / 10000000 - - self.latitude = f_lat - self.longitude = f_lon - self.large_picture = gpx.picture(min_lat, min_lon, max_lat, max_lon, gpx.actual_points) - self.icon_picture = gpx.icon(min_lat, min_lon, max_lat, max_lon) - self.size = gpx.actual_points - self.inserted = true - save! + def migrate_to_storage! + file.attach(:io => File.open(trace_name), + :filename => name, + :content_type => content_type(trace_name), + :identify => false) + + if inserted + image.attach(:io => File.open(large_picture_name), + :filename => "#{id}.gif", + :content_type => "image/gif") + icon.attach(:io => File.open(icon_picture_name), + :filename => "#{id}_icon.gif", + :content_type => "image/gif") end - logger.info "done trace #{id}" + save! - gpx + remove_files end private + def content_type(file) + case Open3.capture2("/usr/bin/file", "-Lbz", file).first.chomp + when /.*\btar archive\b.*\bgzip\b/ then "application/x-tar+gzip" + when /.*\btar archive\b.*\bbzip2\b/ then "application/x-tar+x-bzip2" + when /.*\btar archive\b/ then "application/x-tar" + when /.*\bZip archive\b/ then "application/zip" + when /.*\bXML\b.*\bgzip\b/ then "application/gzip" + when /.*\bXML\b.*\bbzip2\b/ then "application/x-bzip2" + else "application/gpx+xml" + end + end + + def with_trace_file + if file.attached? + file.open do |file| + yield file.path + end + else + yield trace_name + end + end + + def set_filename + file.blob.update(:filename => "#{id}#{extension_name}") if file.attached? + end + def remove_files FileUtils.rm_f(trace_name) FileUtils.rm_f(icon_picture_name) diff --git a/app/models/user.rb b/app/models/user.rb index 8c75b4ef4..7a0d06992 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -45,6 +45,7 @@ class User < ApplicationRecord require "digest" + include AASM has_many :traces, -> { where(:visible => true) } has_many :diary_entries, -> { order(:created_at => :desc) } @@ -86,7 +87,7 @@ class User < ApplicationRecord scope :active, -> { where(:status => %w[active confirmed]) } scope :identifiable, -> { where(:data_public => true) } - has_one_attached :avatar + has_one_attached :avatar, :service => Settings.avatar_storage validates :display_name, :presence => true, :length => 3..255, :exclusion => %w[new terms save confirm confirm-email go_public reset-password forgot-password suspended] @@ -158,6 +159,64 @@ class User < ApplicationRecord user end + aasm :column => :status, :no_direct_assignment => true do + state :pending, :initial => true + state :active + state :confirmed + state :suspended + state :deleted + + # A normal account is active + event :activate do + transitions :from => :pending, :to => :active + end + + # Used in test suite, not something that we would normally need to do. + if Rails.env.test? + event :deactivate do + transitions :from => :active, :to => :pending + end + end + + # To confirm an account is used to override the spam scoring + event :confirm do + transitions :from => [:pending, :active, :suspended], :to => :confirmed + end + + # To unconfirm an account is to make it subject to future spam scoring again + event :unconfirm do + transitions :from => :confirmed, :to => :active + end + + # Accounts can be automatically suspended by spam_check + event :suspend do + transitions :from => [:pending, :active], :to => :suspended + end + + # Unsuspending an account moves it back to active without overriding the spam scoring + event :unsuspend do + transitions :from => :suspended, :to => :active + end + + # Mark the account as deleted but keep all data intact + event :hide do + transitions :from => [:pending, :active, :confirmed, :suspended], :to => :deleted + end + + event :unhide do + transitions :from => [:deleted], :to => :active + end + + # Mark the account as deleted and remove personal data + event :soft_destroy do + before do + remove_personal_data + end + + transitions :from => [:pending, :active, :confirmed, :suspended], :to => :deleted + end + end + def description RichText.new(self[:description_format], self[:description]) end @@ -241,8 +300,8 @@ class User < ApplicationRecord end ## - # destroy a user - leave the account but purge most personal data - def destroy + # remove personal data - leave the account but purge most personal data + def remove_personal_data avatar.purge_later self.display_name = "user_#{id}" @@ -253,7 +312,6 @@ class User < ApplicationRecord self.new_email = nil self.auth_provider = nil self.auth_uid = nil - self.status = "deleted" save end @@ -279,7 +337,7 @@ class User < ApplicationRecord ## # perform a spam check on a user def spam_check - update(:status => "suspended") if status == "active" && spam_score > Settings.spam_threshold + suspend! if may_suspend? && spam_score > Settings.spam_threshold end ## diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 95878402b..8987785da 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -139,30 +139,44 @@