megutech

自身の備忘録として主にWEBサーバー周りの技術について投稿しています。

JavaScriptで日本語入力の文字数制限を行いたい

テキストエリアの入力文字数制限をしたいとき、change eventを拾って入力値をチェックするだけだと、変換を伴う文字入力の際に期待した動きにならなった。

環境

jsなら何でもいいんですが、今回私はreactを使ったのでreact versionを記載します。

Service Version
react 18.2.0

上手く動かなかったコード

import React, { memo, useCallback, useState, type ReactElement } from 'react';

const TEXT_LEN = 5;

function Hoge(): ReactElement {
  const [text, setText] = useState<string>('');

  const handleChangeText = useCallback((e: React.ChangeEvent<HTMLInputElement>): void => {
    const text = e.target.value;
    setText((prevText: string): string => text.substring(0, TEXT_LEN));
  }, []);

  return (
    <input
      name="text"
      type="text"
      value={text}
      onChange={handleChangeText} />
  );
};

export default memo(Hoge);

環境によって挙動が違う。 他のOSやブラウザ、バージョンによっても違うかもしれないが、いずれにしても思った挙動にならないので調査は以下のみ。

OS Version ブラウザ 挙動
Windows 10 Chrome テキスト編集システムを使って指定文字数以上を入力すると、既に入力されている文字を消しながら入力される。
Mac ventura 13.2.1 入力可能文字数内のひらがなのみ入力され、変換を確定しても無視される。
iOS 16.3.1 Safari 入力可能文字数内のひらがなのみ入力され、変換を確定しても無視される。
Android 12 Chrome 変換途中でも文字数が肥えた時点で文字入力が確定され、入力できない。

原因

IME等のようなテキスト編集システムが原因。

対応

composition{start|update|end}でテキスト編集システムの編集セッションイベントが取れる。
これを使えばどうにかできるのではと考え、onCompositionEndで入力を確定させる対応を行ったが、onChangeでの文字入力も同時に行わないと思った通りの挙動にならなかった。

最終的には以下の形に落ち着いた。

import React, { memo, useCallback, useRef, useState, type ReactElement } from 'react';

const TEXT_LEN = 5;

function Hoge(): ReactElement {
  const [text, setText] = useState<string>('');
  const isCompositionStart = useRef<boolean>(false);

  const commitStr = useCallback(() => {
    setText((prevText: string): string => prevText.substring(0, TEXT_LEN));
  }, []);

  const handleCompositionStart = useCallback((): void => { isCompositionStart.current = true; }, []);

  const handleCompositionEnd = useCallback((): void => {
    isCompositionStart.current = false;
    commitStr();
  }, []);

  const handleChangeText = useCallback((e: React.ChangeEvent<HTMLInputElement>): void => {
    setText(e.target.value);
    if (!isCompositionStart.current) {
      commitStr();
    }
  }, [isCompositionStart.current]);

  return (
    <input
      name="text"
      type="text"
      value={text}
      onCompositionStart={handleCompositionStart}
      onCompositionEnd={handleCompositionEnd}
      onChange={handleChangeText} />
  );
};

export default memo(Hoge);

文字入力自体はいかなる状態でも受け入れる。 この対応をしないと変換前の文字で確定したり、意図した挙動とならない。

しかし文字数制限は行いたいので、文字入力後にcommitStrで文字数制限を行う。 ただしテキスト変換システムを使っている場合は、変換確定前に文字数を削ると変換がキャンセルされたりと意図しない挙動となる。

そこでonCompositionStartでテキスト変換中であるかを確認し、もし変換中であれば文字数確定をスキップする。

しかしこのままではテキスト変換システムを使った入力ではテキスト文字数が制限されない。 そのためisCompositionEndで文字数確定を実施する。 また、変換が完了しているため、テキスト変換中フラグを下げる。

所感

文字数制限について調べると、maxlengthを使ったりjsを使ったりという手法は色々出てくるが、IME等での変換が上手くいかない件に関して無視されがちだったので、備忘録をかね残しておくこととする。

もうちょいいい感じにできそうな気がするが、まあ動くしもういいかな。

GroverでHTMLをPDFに変換したら日本語が文字化けして豆腐になっちゃった件

環境

Service Version
OS Amazon Linux 2
Ruby 3.0.2
Ruby on Rails 7.0.3
grover 1.1.1

原因

Groverで使用しているpuppeteerにて日本語化対応が必要だった。

対応

日本語フォントを入れましょう。

$ sudo yum install ipa-gothic-fonts ipa-mincho-fonts ipa-pgothic-fonts ipa-pmincho-fonts

Ruby on RailsでreCAPTCHA Enterprise を使いたい

無料版と有料版のreCAPTCHAとがありますが、使い方は一緒かと思ったら違ってちょっと大変だったお話。

環境

Service Version
Ruby 2.7.4
Ruby on Rails 6.1
recaptcha 5.8.1

前提

Gemはrecaptchaを使います。

通常のreCAPTCHAなら下記用意しておけば、後はドキュメント通りでOKです。

Recaptcha.configure do |config|
  config.site_key  = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy'
  config.secret_key = '6Lc6BAAAAAAAAKN3DRm6VA_xxxxxxxxxxxxxxxxx'

  # Uncomment the following lines if you are using the Enterprise API:
  # config.enterprise = true
  # config.enterprise_api_key = 'AIzvFyE3TU-g4K_Kozr9F1smEzZSGBVOfLKyupA'
  # config.enterprise_project_id = 'my-project'
end

でもenterpriseにはsecret_keyがありません。

代わりにenterprise_api_keyを使います。

ドキュメントにはコメントを外しましょうとはありますが、secret_keyは不要ですとは無かったのでsecret_keyを探して右往左往してしまいました。。

というわけでenterpriseでは以下になります。

Recaptcha.configure do |config|
  config.site_key  = '6Lc6BAAAAAAAAChqRbQZcn_yyyyyyyyyyyyyyyyy'

  config.enterprise = true
  config.enterprise_api_key = 'AIzvFyE3TU-xxxxxxxxxxxxxxxxxxxxxxxxxxxx'
  config.enterprise_project_id = 'my-project'
end

各キーの場所

ついでなのでキーのそれぞれの場所も記しておきます。

これまた探すのに手間取って右往左往しましたので。。。

site_key

GCPの「セキュリティ」から「reCAPTCHAT Enterprise」を選択し、キーを作成してください。

enterprise_api_key

GCPの「APIとサービス」からAPIキーを発行してください。

キーの制限としては「reCAPTCHA Enterprise API」を選択しておいてください。

enterprise_project_id

ダッシュボードの「プロジェクト情報」に記載のプロジェクトIDです。

所感

折角recaptchaにEnterprise用の機能まで備わっているのに、分かりづらくてもったいない。。

公式は公式で google-cloud-recaptcha_enterprise を押してきていて、やたらと混乱してしまった。。。

Devise OmniAuthでサービス先へリダイレクトする前にごにょごにょしたい

OmniAuth、ほとんど何もしなくてもOAuthを実装出来て便利ですよね。

でもサービス側へリダイレクトする直前にごにょごにょしたいことってありませんか?

ありませんか。そうですよね、普通は。。

まあ今回は普通じゃなかったんです。リダイレクト直前にちょっとSessionに色々小細工をしたかったんですが、OmniauthCallbacksController#passthruをオーバーライドしてもうまくいかなかったんで、その方法を共有します。

環境

Service Version
Ruby 3.0.2
Ruby on Rails 6.1
devise 4.8.0
omniauth 2.0.4
omniauth-google-oauth2 1.0.0
omniauth-rails_csrf_protection 1.0.0

対応

最初に書いた通り、リダイレクト寸前にごにょごにょしようとpassthruをオーバーライドしたんですが上手くいかず、それもそのはずコントローラーには到達していなかったんですね。

でもご安心ください。OmniAuthは大変によく完成されたgemです。そういったときのためのメソッドが用意されていました。

今回はリダイレクト時に追加されたparameterをsessionに格納してみましょう。

link_to :OAuth, user_xxxxxx_omniauth_authorize_path(hoge: :fuga)

config/initializers/omniauth.rb

OmniAuth.configure do |config|
  config.before_request_phase = ->(env) {
    request = ActionDispatch::Request.new(env.dup)
    request.session[:fuga] = request.params[:hoge]
  }
end

所感

所感というか愚痴というか。

なぜこういう方法を調べたかというと、別の方がなんとも頓珍漢な方法でOAuthまでにごにょごにょされておりまして、それはあんまりだろうと調べてみた結果こういうメソッドが用意されていたという。。

ちっとは自分で調べるという事くらいしてほしいですね。

削除したActiveStorageへのアクセスは404にしたい

画像の更新などによりActiveStorageのBlobが削除されたにもかかわらず、URLがキャッシュされていたなどの理由により削除された画像にアクセスが来た場合、下記エラーが報告される。

ActiveRecord::RecordNotFoundactive_storage/blobs#show
Couldn't find ActiveStorage::Blob with 'id'=xxx

別段この動きに不満はないが、bugsnagに大量にこのエラーが飛んできて重要なエラー通知に埋もれてしまいかねないので404にしたい。

環境

Service Version
Ruby 2.7.3
Ruby on Rails 6.0.3

対応

ActiveStorageのcontrollerは app/controllers/active_storage/ 以下の対応するcontrollerに置けばオーバーライドできるので、そちらでrescueする。

app/controllers/active_storage/base_controller.rb

# frozen_string_literal: true

# The base class for all Active Storage controllers.
class ActiveStorage::BaseController < ActionController::Base
  include ActiveStorage::SetCurrent

  protect_from_forgery with: :exception

  rescue_from ActiveRecord::RecordNotFound, with: :render_404

  private

  def render_404
    head :not_found
  end
end

所感

オーバーライドは何かあった場合が怖いので、もっといい方法があればそちらにしたい。。。
けど app/controllers/active_storage/blobs_controller.rb には下記のように書いているので、オーバーライドが推奨方法なのかもしれない。

Note: These URLs are publicly accessible. If you need to enforce access protection beyond the security-through-obscurity factor of the signed blob references, you'll need to implement your own authenticated redirection controller.

ActiveStorageの画像ファイルなどを、アンカー要素のdownload属性でダウンロードさせたい

ActiveStorageのservice_urldispositionオプションがデフォルトで:inlineなため、画像やpdfのアンカーにdownload属性をつけていても、ブラウザ上で開いてしまう。

これをダウンロードさせたい。

環境

Service Version
Ruby 2.7.2
Ruby on Rails 6.0.3.4
S3 -

対応

ダウンロードさせたいcontent_typeを config.active_storage.content_types_to_serve_as_binary に追加してあげればいいらしい。

y-yagi.hatenablog.com

別にどこに書いてもいいが、今回はサクッと config/application.rbに書いた。

config.active_storage.content_types_to_serve_as_binary += %w(image/png image/jpeg image/gif image/gif image/bmp application/pdf)

以上です。

送信サーバーと同一ドメインの受信サーバー宛にメールが送信できない

送信サーバーは Amazon Lightsail 、受信サーバーはムームーメールという環境でlLightsailからメール送信ができなくなった。

元々AWSのメール送信制限に引っかかりメールが送信できなくなっており、その調査で /etc/postfix/main.cf はいじっていたのでその辺が怪しいだろうとネットの海を探してみたらまさしくヒット。

環境

Service Version
Amazon Lightsail -
ムームーメール -
Postfix 2.10.1

前提

サーバードメインexample.com とする。

調査

調査の基本はログからと先輩方に叩き込まれたので脳死でまずはログを見る。

ログを見るとどうやら info@example.com 宛が何故か自身にメールを送ってしまっている。

/var/log/maillog

postfix/smtpd[32266]: connect from localhost[127.0.0.1]
postfix/smtpd[32266]: 8A714407E92: client=localhost[127.0.0.1]
postfix/cleanup[32270]: 8A714407E92: message-id=<7e603f861e7c4de87fdb9596a7b15229@example.com>
opendkim[23140]: 8A714407E92: DKIM-Signature field added (s=example, d=example.com)
postfix/qmgr[22693]: 8A714407E92: from=<info@example.com>, size=1489, nrcpt=1 (queue active)
postfix/smtpd[32266]: disconnect from localhost[127.0.0.1]
postfix/local[32271]: 8A714407E92: to=<root@example.com>, orig_to=<info@example.com>, relay=local, delay=0.18, delays=0.1/0.06/0/0.01, dsn=2.0.0, status=sent (delivered to mailbox)

正しくは以下のように mx01.muumuu-mail.com 宛に送って欲しい。

postfix/smtpd[394]: connect from localhost[127.0.0.1]
postfix/smtpd[394]: A981E400056: client=localhost[127.0.0.1]
postfix/cleanup[399]: A981E400056: message-id=<17a46dbb2781b457c2325a1b2db4bae7@example.com>
opendkim[23140]: A981E400056: DKIM-Signature field added (s=example, d=example.com)
postfix/smtpd[394]: disconnect from localhost[127.0.0.1]
postfix/qmgr[391]: A981E400056: from=<info@example.com>, size=1489, nrcpt=1 (queue active)
postfix/smtp[400]: A981E400056: to=<info@example.com>, relay=mx01.muumuu-mail.com[157.7.107.7]:25, delay=0.4, delays=0.11/0.03/0.04/0.21, dsn=2.0.0, status=sent (250 Queued! <17a46dbb2781b457c2325a1b2db4bae7@example.com> (Queue-Id: 115281A40582))

そして試しに送った他ドメイン宛のテストはしっかり送信される。

そこでネットの海を探索した所以下のサイトに巡り合った。本当にありがとうございます。助かりました。

www.lesstep.jp

つまり、mydestination はローカルで受信するドメイン名を指定するのですが、ここで下記のように $mydomain を指定していると今回のような事象になってしまいます。

あ、やったわ。メール送信制限にたどり着くまでにこの対応やったわ。。。

というわけで mydestination から $mydomain を削除。

/etc/postfix/main.cf

- mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
+ mydestination = $myhostname, localhost.$mydomain, localhost

そしてPostfixを再起動させたら、正常に送信されるようになりました。

めでたしめでたし。