tohokuaikiのチラシの裏

技術的ネタとか。

iPhoneの連絡先を編集すると「不正なパスワード cardDAVアカウント"Google“のパスワードを入力 」が出てしまう件の対応

こんなケースですね。

support.google.com

これは、iPhoneの設定の連絡先としてGoogleを追加しているのが原因です。 「設定」アプリから、連絡先>アカウントの部分
f:id:tohokuaiki:20210530174852p:plain

で、このパスワードがおかしいんかな?と思ってGoogleアカウントのいつものパスワードを入力しても全然ダメ…という流れで上記のエラーが出たりします。

f:id:tohokuaiki:20210530175047p:plain

実は、このパスワードは二段階認証が不要なパスワードを独自に発行する必要があります。
通常のログインだとiPhoneの連絡先を編集するたびに二段階認証が来て面倒なためです。

アプリパスワードの作成

Googleアカウントのセキュリティのページに移動します。

Googleへのログイン」の見出しに「アプリパスワード」という項目があるのでクリックします。
f:id:tohokuaiki:20210530175356p:plain

用途があるので、連絡先>iPhoneと選択して「生成」します。
f:id:tohokuaiki:20210530175952p:plain

パスワードが出てくるので、このパスワードを使ってGoogleにログインします。

ちなみに、このパスワードはこれ専用なので削除が可能です。↑のパスワードも削除済みです。

Gulp4で画像のCacheBusterみたいな

今更だけど。

ファイル構造はこんな感じ。

gulpfile.js
gulp-functions.js
package.json
package-lock.json
htdocs/
├── css
│   └── images
├── images
└── js
src
├── javascripts
│   └── vendor
└── scss

src/scss => htdocs/css
src/javascripts => htdocs/js
へとコンパイルされる。

package.json

{
  "name": "bt-gulpfile",
  "version": "1.0.0",
  "description": "",
  "main": "gulpfile.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "private": true,
  "devDependencies": {
    "cheerio": "^1.0.0-rc.3",
    "compass-mixins": "^0.12.10",
    "event-stream": "^4.0.1",
    "gulp": "^4.0.2",
    "gulp-autoprefixer": "^6.1.0",
    "gulp-concat": "^2.6.1",
    "gulp-connect": "^5.7.0",
    "gulp-ejs": "^4.1.1",
    "gulp-htmlmin": "^5.0.1",
    "gulp-if": "^2.0.2",
    "gulp-plumber": "^1.2.1",
    "gulp-postcss": "^8.0.0",
    "gulp-sass": "^4.0.2",
    "gulp-sourcemaps": "^2.6.5",
    "gulp-uglify": "^3.0.2",
    "minimist": "^1.2.0",
    "postcss-cachebuster": "^0.1.6"
  },
  "dependencies": {},
  "browserslist": [
    "last 1 version",
    "ie 9",
    "Android 4.2"
  ]
}

gulpfile.js

var gulp        = require('gulp');
var gulpF       = require('./gulp-functions.js');
var connect     = require('gulp-connect');
var ejs         = require('gulp-ejs');
var uglify      = require("gulp-uglify");
var plumber     = require("gulp-plumber");
var concat      = require("gulp-concat");
var sourcemaps  = require("gulp-sourcemaps");
var autoprefixer= require('gulp-autoprefixer');
var sass        = require('gulp-sass');
var htmlmin     = require('gulp-htmlmin');
var minimist    = require("minimist");
var gulpif      = require('gulp-if');
var postcss     = require('gulp-postcss');
var cachebuster = require('postcss-cachebuster');
const fs        = require('fs');

var options = minimist(process.argv.slice(2), {
  string: 'env',
  default: { env: 'production' }
});

const config = {
    production: options.env !== 'dev',
    sourceMaps: options.env === 'dev'
};

var packageJson = JSON.parse(fs.readFileSync('./package.json'));

gulp.task('javascript', function(cb){
    gulp.src(['./src/javascripts/vendor/*.js', './src/javascripts/*.js'])
      .pipe(plumber())
        .pipe(gulpif(config.sourceMaps, sourcemaps.init()))
          .pipe(concat('dest.js'))
            .pipe(uglify())
              .pipe(gulpif(config.sourceMaps, sourcemaps.write()))
                .pipe(gulp.dest("htdocs/js/"));
    cb();
});

gulp.task('sass', function (cb) {
    gulp.src(['./src/scss/*.scss', './src/scss/vendor/*.scss'])
      .pipe(plumber())
        .pipe(gulpif(config.sourceMaps, sourcemaps.init()))
          .pipe(concat('style.css'))
            .pipe(sass({
              outputStyle: 'compressed',
            }))
              .pipe(autoprefixer({
                cascade: false
              }))
                .pipe(postcss([
                    cachebuster({
                      type: 'checksum',
                      cssPath : '/htdocs/css',
                    }),
                    ]))
                  .pipe(gulpif(config.sourceMaps, sourcemaps.write()))
                    .pipe(gulp.dest('./htdocs/css/'));
    cb();
});

gulp.task('html', function (cb) {
    gulp.src(['./src/**/*.html', '!./src/**/_*.html'])
      .pipe(plumber())
        .pipe(ejs())
          .pipe(gulpF.htmlrev())
            .pipe(htmlmin({
              collapseWhitespace : false,
              removeComments : true
            }))
              .pipe(gulp.dest('./htdocs'));
    cb();
});

gulp.task('watch', function(cb){
    // auto compile
    gulp.watch(
        ['./src/**/*.html',
         './src/javascripts/**/*.js',
         './src/scss/**/*.scss',
         ],
        gulp.series('html', 'javascript', 'sass'));
    
    // browser live reload
    gulp.watch('htdocs').on('change', function(filepath){
        gulp.src(filepath, { read: false }).pipe(connect.reload());
    });
    cb();
});

gulp.task('connect', function(done) {
  connect.server({
    root: 'htdocs',
    livereload: true
  }, function () { this.server.on('close', done) });
});

// only-compile
gulp.task('compile', gulp.series(
    'html', 'javascript', 'sass'));

// dev
gulp.task('default', gulp.series(
    'html', 'javascript', 'sass',
    'watch', 'connect'));

gulp-functions.js

var eventstream = require('event-stream');
const cheerio   = require('cheerio');
const path      = require('path');
const fs        = require('fs');
const crypto    = require('crypto');

module.exports = {
    // htmlファイルのimg/script/linkのファイルにmd5値でリビジョンを付ける
    htmlrev : ()=>{
        return eventstream.map(function(file, cb) {
            var $ = cheerio.load(file.contents.toString(),
                                 { decodeEntities: false });
            $('link[rel="stylesheet"],script[src],img').each(function(i, ele) {
                let attrname = "";
                let hash;
                switch (ele.name.toLowerCase()){
                  case 'link':
                    attrname = 'href';
                    break;
                  case 'img':
                  case 'script':
                    attrname = 'src';
                    break;
                }
                if (attrname) {
                    let src = $(ele).attr(attrname);
                    if (!src.match(new RegExp('^https?:\/\/'))) {
                        var normPath = path.normalize(src);
                        if (normPath.indexOf(path.sep) === 0) {
                            dependencyPath = path.join(file.base, normPath);
                        }
                        else {
                            dependencyPath = path.resolve(path.dirname(file.path), normPath);
                        }
                        let hasQS = dependencyPath.lastIndexOf("?");
                        if (hasQS >= 0){
                            dependencyPath = dependencyPath.substring(0, hasQS);
                        }
                        // replace src => htdocs
                        dependencyPath = dependencyPath.replace(
                            path.sep+'src'+path.sep,
                            path.sep+'htdocs'+path.sep);
                        try {
                            data = fs.readFileSync(dependencyPath);
                            hash = crypto.createHash('md5');
                            hash.update(data.toString(), 'utf8');
                            $(ele).attr(attrname,
                                        $(ele).attr(attrname) + '?rev=' + hash.digest('hex'));
                        }
                        catch(e) {
                            // fail silently.
                            console.log('[ERROR] revision attach failed: ' + e.message);
                        }
                    }
                }
            });
            file.contents =
              new Buffer.from($.root().html());
            cb(null, file);
        });
    }
}

実行する

npxで実行

$ npx gulp watch

でファイル監視の開発モード

$ npx gulp compile

コンパイルしてくれる。

PHPでCSVファイルを書き出す時にBOMを付けて改行コードをCRLFにする

Streamってあんまり使わない…

こんな感じ。

<?php
use App\Helpers\CrlfFilter;
# Streamを開く
$stream = fopen('php://output', 'w');
 # BOMを付ける
fputs($stream, pack('C*',0xEF,0xBB,0xBF));
# 改行コード変換フィルターを登録
stream_filter_register('CrlfFilter', CrlfFilter::class); 
# 登録したフィルターを適用
stream_filter_prepend($stream,'CrlfFilter', STREAM_FILTER_WRITE);
#データ書き込み
fputcsv($stream, [1,2,3];
# 終了
fclose($stream);

stream_filterって初めて使った。

ちなみにCrlfFilterは

こちらからコピペした

<?php
namespace App\Helpers;

/**
 * Class CrlfFilter
 * @package App\Helpers
 * @brief 改行コードをLFからCRLFにする
 */
class CrlfFilter extends \php_user_filter
{
    public function filter($in, $out, &$consumed, $closing) {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $bucket->data = preg_replace("/\n$/", '', $bucket->data);
            $bucket->data = preg_replace("/\r$/", '', $bucket->data);
            $bucket->data = $bucket->data . "\r\n";
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

webpackでSASSとJavaScriptをそれぞれ別々のファイルにコンパイルしたかっただけの人生だった

今まで、SASSを使うときは1つの.scssファイルにまとめて

$ ./node_modules/node-sass/bin/node-sass ./scss/foo.scss ./htdocs/css/foo.css --output-style compressed --source-map true &

とかやってたんだけど、JavaScriptも一緒にコンパイルしたいなと。TypeScriptじゃなくてJavaScriptなんだけど、npm installとかしたやつを一緒にコンパイルしたくなったんです。今までは、Laravel mix使ってたんで素で使うっというのが無くて…

とりあえず、package.json

{
  "name": "myproject-sass",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "prod": "NODE_ENV=production webpack --mode=production",
    "dev": "NODE_ENV=development webpack --mode=development --watch"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "css-loader": "^5.1.2",
    "mini-css-extract-plugin": "^1.3.9",
    "node-sass": "^5.0.0",
    "sass-loader": "^11.0.1",
    "webpack": "^5.25.1",
    "webpack-cli": "^4.5.0",
    "webpack-fix-style-only-entries": "^0.6.1"
  },
  "dependencies": {
    "bs-custom-file-input": "^1.3.4"
  }
}

node-sass使ってるのは、いにしえのSCSSライブラリcompassを使ってるからである。

当初、sass-loaderさえあればcss-loaderいらんやろ、CSSなんて書いて無いしって思ったんだけど、いるのね。

webpack.config.js

const path = require('path');
const MiniCSSExtractPlugin = require("mini-css-extract-plugin");
const FixStyleOnlyEntries = require("webpack-fix-style-only-entries");
const enabledSourceMap =  process.env.NODE_ENV !== 'production';

module.exports = ( env, argv ) => ({
  entry: {
      "foo_script": './src/js/foo.js',
      "foo_style": './src/scss/foo.scss'
  },
  output: {
    filename: 'js/[name].js',
    path: path.resolve(__dirname, 'htdocs')
  },
  module: {
    rules: [
        {
          test: /\.(sa|sc|c)ss$/,
          use: [
              {
                loader: MiniCSSExtractPlugin.loader,
              },
              {
                loader: "css-loader",
                options: {
                  url: false,
                  sourceMap: enabledSourceMap
                }
              },
              {
                loader: "sass-loader",
                options: {
                  sourceMap: enabledSourceMap
                }
              }
              ]
        }]
  },
  plugins: [
    new FixStyleOnlyEntries(),
    new MiniCSSExtractPlugin({
      filename: "./css/[name].css"
    })
  ],
  devtool: "inline-source-map",
});

ハマりポイント

  • devtoolを付けないと、sourceMapが有効にならなかった。
  • 本当は、foo.js foo.cssってしたかったんだけど、するとentryに同じキーになるのでできなかった。
  • じゃあ、outputのfilenameでfoo.css/foo.jsってそれぞれ指定すればいいじゃんって思ったんだけど、そうするとエラーが出てうまくいかなかった。多分、CSSにする際に一旦JavaScriptを経由してて、そのJavaScriptのファイル名が被ってしまうせいだと思う。
  • 環境変数NODE_ENVは、NODE_ENV=development ってpackage.jsonで指定しないといけなかったのを知らんかった。

これで、npm run prod/devで快適にSCSSとJavaScriptが書ける。・

コンストラクタでPHPのクラスの確認をして、interfaceでなくてもnewできないクラスを作る

interface指定するとプロパティが使えなくなってしまうのが嫌なんですよ。

ということで、selfとget_classを使ってこんな感じ。

<?php
namespace Foo;
class ArgsP // このクラスはインスタンスを作らせたくない。
{
    function __construct(... $args){
        if (self::class === get_class($this)){
            throw new \Exception('クラスは作れません。');
        }
    }
}
class ArgsC extends ArgsP
{
    function __construct(... $args){
        parent::__construct(... $args);
    }
}
$a = new ArgsC(1, 2, 3); // O.K
$b = new ArgsP(1, 2, 3); // 例外発生