From f3797b796d92fa9f9af6ca86b176f62f6eefbc36 Mon Sep 17 00:00:00 2001 From: chiu Date: Thu, 28 May 2020 22:06:56 +0800 Subject: [PATCH] first commit --- .rspec | 3 + .travis.yml | 8 ++ CODE_OF_CONDUCT.md | 74 ++++++++++++++ Gemfile | 4 + Gemfile.lock | 35 +++++++ LICENSE.txt | 21 ++++ README.md | 164 ++++++++++++++++++++++++++++++++ Rakefile | 6 ++ bin/console | 14 +++ bin/setup | 8 ++ lib/repost.rb | 9 ++ lib/repost/action.rb | 8 ++ lib/repost/extend_controller.rb | 23 +++++ lib/repost/senpai.rb | 79 +++++++++++++++ lib/repost/version.rb | 3 + repost.gemspec | 37 +++++++ spec/repost_spec.rb | 5 + spec/senpai_spec.rb | 96 +++++++++++++++++++ spec/spec_helper.rb | 15 +++ 19 files changed, 612 insertions(+) create mode 100644 .rspec create mode 100644 .travis.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 Rakefile create mode 100644 bin/console create mode 100644 bin/setup create mode 100644 lib/repost.rb create mode 100644 lib/repost/action.rb create mode 100644 lib/repost/extend_controller.rb create mode 100644 lib/repost/senpai.rb create mode 100644 lib/repost/version.rb create mode 100644 repost.gemspec create mode 100644 spec/repost_spec.rb create mode 100644 spec/senpai_spec.rb create mode 100644 spec/spec_helper.rb diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..34c5164 --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--format documentation +--color +--require spec_helper diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..de4d7ac --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +--- +sudo: false +language: ruby +cache: bundler +rvm: + - 2.5.0 + - 2.3.8 +before_install: gem install bundler -v 2.0.2 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..b1cc7f3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at yoslavskiy@innocode.no. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..b742d36 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +# Specify your gem's dependencies in repost.gemspec +gemspec diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..54a97af --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,35 @@ +PATH + remote: . + specs: + repost (0.3.1) + +GEM + remote: https://rubygems.org/ + specs: + diff-lcs (1.3) + rake (10.5.0) + rspec (3.8.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) + rspec-core (3.8.0) + rspec-support (~> 3.8.0) + rspec-expectations (3.8.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-mocks (3.8.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-support (3.8.0) + +PLATFORMS + ruby + +DEPENDENCIES + bundler (~> 2.0) + rake (~> 10.0) + repost! + rspec (~> 3.0) + +BUNDLED WITH + 2.0.2 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..3d6d5b2 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 YaroslavO + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..eb0fc2c --- /dev/null +++ b/README.md @@ -0,0 +1,164 @@ +

+ + +

+ +

+ + +

+ +Gem **Repost** implements Redirect using POST method + +[![Gem Version](https://badge.fury.io/rb/repost.svg)](https://badge.fury.io/rb/repost) +[![Build Status](https://travis-ci.org/vergilet/repost.svg?branch=master)](https://travis-ci.org/vergilet/repost) + +## Installation + +Add this line to your application's Gemfile: + +```ruby +gem 'repost' +``` + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install repost + + + +## Diagram + +What problem does it solve? + +When you need to send some parameters to an endpoint which should redirect you after execution. There wouldn't be a problem if an endpoint receives [GET], because you can just use `redirect_to post_url(id: @model.id, token: model.token...)`. + +But when an endpoint receives [POST], you have to generate html form and submit it. So `repost` gem helps to avoid creation of additional view with html form, just use `redirect_post` method instead. +I faced with this problem when was dealing with bank transactions. You can see the approximate scheme: + +

+ + +

+ + +## Usage + +If you use Rails, gem automatically includes helper methods to your controllers: + +```ruby +repost(...) +``` +and, as an alias + +```ruby +redirect_post(...) +``` + +*Under the hood it calls `render` method of current controller with `html:`.* + +### Example in Rails app: + +```ruby +class MyController < ApplicationController + ... + def index + repost(...) + end + ... +end +``` +______________ + +If you use Sinatra, Roda or etc., you need to require it first somewhere in you project: + +```ruby +require 'repost' +``` + +Then ask your senpai to generate a string with html: + + +```ruby +Repost::Senpai.perform(...) +``` + +### Example in Sinatra, Roda, etc. app: + +```ruby +class MyController < Sinatra::Base + get '/' do + Repost::Senpai.perform(...) + end +end +``` + + + +#### *Reminder:* + +- *In Rails app use `repost` or `redirect_post` method in your controller which performs 'redirect' when it is called.* + +- *In Sinatra, Roda, etc. app or if you need html output - call Senpai* + + +#### Full example: + +*UPD: authenticity token is **turned off** by default. Use `:auto` or `'auto'` to turn on default authenticity token from Rails. Any other string value would be treated as custom auth token value.* + +```ruby +Repost::Senpai.perform('http://examp.io/endpoint', # URL, looks understandable + params: {a: 1, b: 2, c: '3', d: "4"}, # Your request body + options: { + method: :post, # OPTIONAL - DEFAULT is :post, but you can use others if needed + authenticity_token: 'auto', # OPTIONAL - :auto or 'auto' for Rails form_authenticity_token, string - custom token + charset: 'Windows-1251', # OPTIONAL - DEFAULT is "UTF-8", corresponds for accept-charset + form_id: 'CustomFormID', # OPTIONAL - DEFAULT is autogenerated + autosubmit: false, # OPTIONAL - DEFAULT is true, if you want to get a confirmation for redirect + decor: { # If autosubmit is turned off or Javascript is disabled on client + section: { # ... you can decorate confirmation section and button + classes: 'red-bg red-text', # OPTIONAL -
section, set classNames, separate with space + html: '

Press this button, dude!

' # OPTIONAL - Any html, which will appear before submit button + }, + submit: { + classes: 'button-decorated round-border', # OPTIONAL - with type submit, set classNames, separate with space + text: 'c0n71nue ...' # OPTIONAL - DEFAULT is 'Continue' + } + } + } +) + +``` + +### Authenticity Token (Rails) + +Currently you can pass the **authenticity token** in two ways: + +* Recommended: + + *Use `options` and `:auto` to pass the auth token. That should protect you from any implementation changes in future Rails versions* + + ```ruby + redirect_post('https://exmaple.io/endpoint', options: {authenticity_token: :auto}) + ``` +* Or, it is still valid to: + + *use `params` and `form_authenticity_token` method directly from ActionController* + ```ruby + redirect_post('https://exmaple.io/endpoint', params: {authenticity_token: form_authenticity_token}) + ``` + + + +## License +The gem is available as open source under the terms of the MIT License. + +Copyright © 2019 Yaro. + +[![GitHub license](https://img.shields.io/badge/license-MIT-brightgreen)](https://raw.githubusercontent.com/vergilet/repost/master/LICENSE.txt) + +**That's all folks.** diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..b7e9ed5 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +require "bundler/gem_tasks" +require "rspec/core/rake_task" + +RSpec::Core::RakeTask.new(:spec) + +task :default => :spec diff --git a/bin/console b/bin/console new file mode 100644 index 0000000..eebbb80 --- /dev/null +++ b/bin/console @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby + +require "bundler/setup" +require "repost" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require "irb" +IRB.start(__FILE__) diff --git a/bin/setup b/bin/setup new file mode 100644 index 0000000..dce67d8 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/lib/repost.rb b/lib/repost.rb new file mode 100644 index 0000000..2982aab --- /dev/null +++ b/lib/repost.rb @@ -0,0 +1,9 @@ +require "repost/version" +require "repost/action" +require "repost/senpai" +require "repost/extend_controller" + +module Repost + class Error < StandardError; end +end + diff --git a/lib/repost/action.rb b/lib/repost/action.rb new file mode 100644 index 0000000..19f28e0 --- /dev/null +++ b/lib/repost/action.rb @@ -0,0 +1,8 @@ +module Repost + class Action + def self.perform(*args) + action = new(*args) + action.perform + end + end +end diff --git a/lib/repost/extend_controller.rb b/lib/repost/extend_controller.rb new file mode 100644 index 0000000..d2fa759 --- /dev/null +++ b/lib/repost/extend_controller.rb @@ -0,0 +1,23 @@ +if defined?(Rails) && defined?(ActiveSupport) + ActiveSupport.on_load(:action_controller) do + class ::ActionController::Base + + def repost(url, params: {}, options: {}) + authenticity_token = form_authenticity_token if ['auto', :auto].include?(options[:authenticity_token]) + render html: Repost::Senpai.perform( + url, + params: params, + options: options.merge({authenticity_token: authenticity_token}.compact) + ).html_safe + end + + alias :redirect_post :repost + + end + end +end + +# Sinatra & Rack Protection +# TODO +# defined?(Sinatra::Base) && defined?(Rack::Protection::AuthenticityToken) +# env&.fetch('rack.session', :csrf) diff --git a/lib/repost/senpai.rb b/lib/repost/senpai.rb new file mode 100644 index 0000000..4b77a5a --- /dev/null +++ b/lib/repost/senpai.rb @@ -0,0 +1,79 @@ +module Repost + class Senpai < Action + DEFAULT_SUBMIT_BUTTON_TEXT = 'Continue' + DEFAULT_CHARSET = 'UTF-8' + + def initialize(url, params: {}, options: {}) + @url = url + @params = params + @options = options + @method = options.fetch(:method, :post) + @authenticity_token = options.fetch(:authenticity_token, nil) + @charset = options.fetch(:charset, DEFAULT_CHARSET) + @form_id = options.fetch(:form_id, generated_form_id) + @autosubmit = options.fetch(:autosubmit, true) + @section_classes = nil#options.dig(:decor, :section, :classes) + @section_html = nil#options.dig(:decor, :section, :html) + @submit_classes = nil#options.dig(:decor, :submit, :classes) + @submit_text = DEFAULT_SUBMIT_BUTTON_TEXT#options.dig(:decor, :submit, :text) || DEFAULT_SUBMIT_BUTTON_TEXT + end + + def perform + compiled_body = if autosubmit + form_body << auto_submit_script << no_script + else + form_body << submit_section + end + form_head << compiled_body << form_footer + end + + private + + attr_reader :url, :params, :options, :method, :form_id, :autosubmit, + :section_classes, :section_html, :submit_classes, + :submit_text, :authenticity_token, :charset + + def form_head + "
" + end + + def form_body + inputs = params.map do |key, value| + "" + end + inputs.unshift(csrf_token) if authenticity_token + inputs.join + end + + def form_footer + "
" + end + + def csrf_token + "" + end + + def no_script + "" + end + + def submit_section + "
+ #{section_html} + +
" + end + + def generated_form_id + "repost-#{Time.now.to_i}" + end + + def auto_submit_script + "" + end + end +end diff --git a/lib/repost/version.rb b/lib/repost/version.rb new file mode 100644 index 0000000..5d19d9a --- /dev/null +++ b/lib/repost/version.rb @@ -0,0 +1,3 @@ +module Repost + VERSION = "0.3.1" +end diff --git a/repost.gemspec b/repost.gemspec new file mode 100644 index 0000000..a6867e1 --- /dev/null +++ b/repost.gemspec @@ -0,0 +1,37 @@ + +lib = File.expand_path("../lib", __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require "repost/version" + +Gem::Specification.new do |spec| + spec.name = "repost" + spec.version = Repost::VERSION + spec.authors = ["YaroslavO"] + spec.email = ["osyaroslav@gmail.com"] + + spec.summary = %q{Gem implements Redirect using POST method} + spec.description = %q{Helps to make POST 'redirect', but actually builds [form] with method: :post under the hood} + spec.homepage = "https://vergilet.github.io/repost/" + spec.license = "MIT" + + # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' + # to allow pushing to a single host or delete this section to allow pushing to any host. + if spec.respond_to?(:metadata) + + spec.metadata["homepage_uri"] = spec.homepage + # spec.metadata["source_code_uri"] = "Put your gem's public repo URL here." + # spec.metadata["changelog_uri"] = "Put your gem's CHANGELOG.md URL here." + else + raise "RubyGems 2.0 or newer is required to protect against " \ + "public gem pushes." + end + + spec.files = Dir['{app,config,db,lib}/**/*', 'MIT-LICENSE', 'Rakefile', 'README.rdoc'] + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.add_development_dependency "bundler", "~> 2.0" + spec.add_development_dependency "rake", "~> 10.0" + spec.add_development_dependency "rspec", "~> 3.0" +end diff --git a/spec/repost_spec.rb b/spec/repost_spec.rb new file mode 100644 index 0000000..17b1110 --- /dev/null +++ b/spec/repost_spec.rb @@ -0,0 +1,5 @@ +RSpec.describe Repost do + it "has a version number" do + expect(Repost::VERSION).not_to be nil + end +end diff --git a/spec/senpai_spec.rb b/spec/senpai_spec.rb new file mode 100644 index 0000000..825d534 --- /dev/null +++ b/spec/senpai_spec.rb @@ -0,0 +1,96 @@ +require 'spec_helper' + +RSpec.describe Repost::Senpai do + let(:url) { 'http://example.com/endpoint' } + let(:html) { described_class.perform(url) } + + it 'generates post form' do + aggregate_failures do + expect(html).to include('form') + expect(html).to include(url) + expect(html).to include("type='submit'") + end + end + + it 'autosubmit form by default' do + expect(html).to include('.submit()') + end + + describe 'with params' do + let(:params) do + { + name: 'TestName', + description: 'Some cool description', + count: 696, + string_size: '234', + boolean: true, + string_boolean: 'false' + } + end + + let(:html) { described_class.perform(url, params: params) } + + it 'generates post form' do + aggregate_failures do + expect(html).to include("input type='hidden'") + expect(html).to include("value='#{params[:name]}'") + expect(html).to include("value='#{params[:description]}'") + expect(html).to include("value='#{params[:string_size]}'") + expect(html).to include("value='#{params[:string_boolean]}'") + + expect(html).to include("value='#{params[:count]}'") + expect(html).to include("value='#{params[:boolean]}'") + end + end + end + + describe 'with options' do + let(:options) { {} } + let(:html) { described_class.perform(url, options: options) } + + context 'empty options' do + it 'autosubmit form by default' do + expect(html).to include('.submit()') + end + end + + context 'set options' do + describe 'autosubmit' do + context 'enabled' do + + let(:options) do + { + autosubmit: true + } + end + + it 'returns submit function' do + aggregate_failures do + expect(html).to include('.submit()') + expect(html).to include('