2020年 4月 の投稿一覧

ec-cube4 タグ 検索 – ECCUBE4の商品検索で商品タグも条件に追加するANDじゃなくてORで – Customizer(Where| Query)だと出来ない?

EC-Cube4で商品検索でタグも検索条件に追加したいときどうすれば出来るのかいろいろ試してみました。

そもそも商品検索のクエリにdtb_product_tagが含まれていないので、WhereCustomizerではなくQueryCustomizerで下記のように作りました。

namespace Customize\Repository;

use Doctrine\ORM\QueryBuilder;
use Eccube\Doctrine\Query\QueryCustomizer;
use Eccube\Repository\QueryKey;

class TagSearchQueryCustomizer implements QueryCustomizer {

    protected $tag;

    public function __construct(TagRepository $tagRepository){
        $this->tag = $tagRepository;
    }

    public function getQueryKey() {
        return QueryKey::PRODUCT_SEARCH;
    }

    public function customize(QueryBuilder $builder, $params, $queryKey){
        if ($params['name']) {

            $builder->innerJoin("p.ProductTag", "pt");

            $tags = $this->tag->createQueryBuilder('t')
                    ->where("NORMALIZE( t.name ) LIKE NORMALIZE( :name )")
                    ->setParameter("name", '%' . $params['name'] . '%')
                    ->getQuery()->getResult();

            foreach ($tags as $tag) {
                $builder->orWhere("pt.Tag = :TagId");
                $builder->setParameter("TagId", $tag->getId());
            }
        }
    }
}

タグレポジトリからLIKE検索した結果のタグIDをキーに検索してくれるのですが、これだと下記のようなクエリになってしまい、表示属性や廃止属性を無視してしまいます。

FROM
    dtb_product d0_
    INNER JOIN
        dtb_product_class d1_
    ON  d0_.id = d1_.product_id
    AND d1_.discriminator_type IN('productclass')
    INNER JOIN
        dtb_product_tag d2_
    ON  d0_.id = d2_.product_id
    AND d2_.discriminator_type IN('producttag')
WHERE
    (
        (
            d0_.product_status_id = 1
        AND (
                CONVERT(d0_.name USING utf8) COLLATE utf8_unicode_ci LIKE CONVERT(? USING utf8) COLLATE utf8_unicode_ci
            OR  CONVERT(d0_.search_word USING utf8) COLLATE utf8_unicode_ci LIKE CONVERT(? USING utf8) COLLATE utf8_unicode_ci
            OR  EXISTS(
                    SELECT
                        d3_.id
                    FROM
                        dtb_product_class d3_
                    WHERE
                        (
                            d0_.id = d3_.product_id
                        AND CONVERT(d3_.product_code USING utf8) COLLATE utf8_unicode_ci LIKE CONVERT(? USING utf8) COLLATE utf8_unicode_ci
                        )
                    AND d3_.discriminator_type IN('productclass')
                )
            )
        AND d1_.visible = 1
        )
    OR  d2_.tag_id = ?
    )
AND d0_.discriminator_type IN('product')

search_wordやnameをみているグルーピングの中にORでタグ条件を入れたいのですが、QueryCustomizerだと入れ込めませんでした。。

次にWhereCustomizerならsearch_wordやnameの部分に入れ込めるか試してみました。

<?php
namespace Customize\Repository;

use Eccube\Doctrine\Query\WhereClause;
use Eccube\Doctrine\Query\WhereCustomizer;
use Eccube\Repository\QueryKey;
use Eccube\Repository\TagRepository;

class TagSearchWhereCustomizer extends WhereCustomizer {

    protected $tag;

    public function __construct(TagRepository $tagRepository) {
        $this->tag = $tagRepository;
    }

    public function getQueryKey() {
        return QueryKey::PRODUCT_SEARCH;
    }

    protected function createStatements($params, $queryKey) {

        $whereCluse = [];

        if ($params['name']) {

            $tags = $this->tag->createQueryBuilder('t')
                ->where("NORMALIZE( t.name ) LIKE NORMALIZE( :name )")
                ->setParameter("name", '%' . $params['name'] . '%')
                ->getQuery()->getResult();

            // foreach ($tags as $tag) {
            //  // $whereCluse[] = WhereClause::eq("pt.Tag", ':Tag', $tag->getId());
            // }
        }

        return [
            WhereClause::isNull('p.note'),
        ];

        // return $whereCluse;
    }

}

一旦テストとして、dtb_productのノート属性を検索条件にしてcreateStatementsの戻り値がクエリのどこに追加されるのか確認してみました。

 INNER JOIN
        dtb_product_tag d2_
    ON  d0_.id = d2_.product_id
    AND d2_.discriminator_type IN('producttag')
WHERE
    (
        d0_.product_status_id = 1
    AND (
            CONVERT(d0_.name USING utf8) COLLATE utf8_unicode_ci LIKE CONVERT(? USING utf8) COLLATE utf8_unicode_ci
        OR  CONVERT(d0_.search_word USING utf8) COLLATE utf8_unicode_ci LIKE CONVERT(? USING utf8) COLLATE utf8_unicode_ci
        OR  EXISTS(
                SELECT
                    d3_.id
                FROM
                    dtb_product_class d3_
                WHERE
                    (
                        d0_.id = d3_.product_id
                    AND CONVERT(d3_.product_code USING utf8) COLLATE utf8_unicode_ci LIKE CONVERT(? USING utf8) COLLATE utf8_unicode_ci
                    )
                AND d3_.discriminator_type IN('productclass')
            )
        )
    AND d1_.visible = 1
    AND d0_.note IS NULL

WhereCustomizerでも思った部分にはクエリは追加されませんでした。QueryCustomizerでinnerJoinして、WhereCustomizerで条件追加という作戦は失敗のようです。

ここに来てやっとProductRepositoryのソースをみてみると、getQueryBuilderBySearchDataの中でandWhereで追加したいグループが追加された後、customizeが呼ばれているので、自分が入れ込みたい場所にいれるにはProductRepositoryをいじる必要があるのかもしれない。。

    public function getQueryBuilderBySearchData($searchData)
    {
        $qb = $this->createQueryBuilder('p')
            ->andWhere('p.Status = 1');

        // category
        $categoryJoin = false;
        if (!empty($searchData['category_id']) && $searchData['category_id']) {
            $Categories = $searchData['category_id']->getSelfAndDescendants();
            if ($Categories) {
                $qb
                    ->innerJoin('p.ProductCategories', 'pct')
                    ->innerJoin('pct.Category', 'c')
                    ->andWhere($qb->expr()->in('pct.Category', ':Categories'))
                    ->setParameter('Categories', $Categories);
                $categoryJoin = true;
            }
        }

        // name
        if (isset($searchData['name']) && StringUtil::isNotBlank($searchData['name'])) {
            $keywords = preg_split('/[\s ]+/u', str_replace(['%', '_'], ['\\%', '\\_'], $searchData['name']), -1, PREG_SPLIT_NO_EMPTY);

            foreach ($keywords as $index => $keyword) {
                $key = sprintf('keyword%s', $index);
                $qb
                    ->andWhere(sprintf('NORMALIZE(p.name) LIKE NORMALIZE(:%s) OR 
                        NORMALIZE(p.search_word) LIKE NORMALIZE(:%s) OR 
                        EXISTS (SELECT wpc%d FROM \Eccube\Entity\ProductClass wpc%d WHERE p = wpc%d.Product AND NORMALIZE(wpc%d.code) LIKE NORMALIZE(:%s))',
                        $key, $key, $index, $index, $index, $index, $key))
                    ->setParameter($key, '%'.$keyword.'%');
            }
        }

並び替えなど。。。

 return $this->queries->customize(QueryKey::PRODUCT_SEARCH, $qb, $searchData);
    }

ECCUBEの作法というかいろいろわかってないけど、ProductRepository直接編集ってNGな気がするけど、どうするのが正しいんだろうか。。

もしProductRepositoryを修正していいならこんな感じでできたけど、proxyとかでやるのかな?

foreach ($keywords as $index => $keyword) {
                $key = sprintf('keyword%s', $index);
                $qb
                    ->andWhere(sprintf('NORMALIZE(p.name) LIKE NORMALIZE(:%s) OR 
                        NORMALIZE(p.search_word) LIKE NORMALIZE(:%s) OR 
                        EXISTS (SELECT wpc%d FROM \Eccube\Entity\ProductClass wpc%d WHERE p = wpc%d.Product AND NORMALIZE(wpc%d.code) LIKE NORMALIZE(:%s)) OR
                        EXISTS (
                            SELECT eept%d FROM \Eccube\Entity\ProductTag eept%d
                            WHERE p = eept%d.Product
                            AND eept%d.Tag IN (
                                SELECT eet%d FROM \Eccube\Entity\Tag eet%d
                                WHERE NORMALIZE( eet%d.name ) LIKE NORMALIZE( :%s )
                            )
                        )',
						$key, $key, $index, $index, $index, $index, $key, 
						$index, $index, $index, $index, $index, $index, $index, $key))
                    ->setParameter($key, '%'.$keyword.'%');

traitの例はカラム追加とかが多くて、メソッド書き換の方法を調べてみます。

iOS一部のユーザだけWKWebViewでエラー( NSErr.code 105 )が発生する – アクセスログは存在するがdidFailProvisionalNavigation

func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
        let nserr = error as NSError

NSLog("didFailProvisionalNavigation :\(nserr.code)")

iOSのアプリでWKWebViewを利用して表示しているページが一部のユーザだけ表示できず、didFailProvisionalNavigation Error 105が発生するので原因調査

自分の環境では発生しないので、一般ユーザから動作ログをレポートしてもらう機能をつけてから、アクセスログと突合させてみると、didFailProvisionalNavigationが発生した場合、アプリに同梱したHTMLを表示させているが、そのHTMLが表示された後、すぐにアクセス失敗したURLのパラメータ欠損みたいなURLに自動的に再度読み込まれる動きをしていることがわかりました。

didFailProvisionalNavigationで発生しているのは105で検索しても情報があまり見つからなかったが、下記のスレッドを発見しました。

https://bugs.chromium.org/p/chromium/issues/detail?id=831381

どうやらペアレントコントロールの影響のようで、Code 105で動きが今回の件と似ています、当初は広告ブロックも疑いましたが、SFSafariViewControllerではなく、wkWebviewなので除外していましたが、ペアレントコントロールはwkWebviewでも動作するようで、多分これです。

試しに再現するユーザー直接連絡してsafariでwkWebviewでエラーとなるページを開いてもらいましたが、アクセスできないとのことでした。

試した限りはコンテンツの内容ではなく、URLのそれっぽい文字列のようでしたので、別のURLを用意してリリースして対応

nodemailerでISO-2022-JPのメールを送信したい( Mailparserじゃなくて送信)

メールの受信プログラムのテストでマルチパートやUTFだけじゃなく、iso-2022-jpのメールも正しく受信できるか試したかったので、nodemailerで送信しようと思いましたが、どうやらnodemailerがiso-2022-jpに対応していない。。

解析するmailparserの例は見つかるけど、iso-2022-jpで送信するサンプルが見つからないので、試した結果、なんんとかiso-2022-jpで送信でき、Yahooメール、Gmailなど主要なサービスでも正しく表示されました。

iconvで変換したiso-2022-jpの文字列をsubjectなどに渡しても、「Content-Type: text/plain; charset=ISO-2022-JP」にはなりません、Content-TypeはUTF8で本文などがiso-2022-jpで文字化けします。

なのでrawを使って強制的にパラメータを指定します。

let message = {
    envelope: {
        from: "from@gmail.com",
        to: ["to@gmail.com"]
    },
    raw: `Content-Type: text/plain; charset=ISO-2022-JP
From: =?ISO-2022-JP?B?` + new Buffer(iconv.convert("nodemailerです").toString()).toString("base64") + `=?= <from@gmail.com>,
To: to@gmail.com
Subject: =?ISO-2022-JP?B?` + new Buffer(iconv.convert("ISO-2022-JPのメール").toString()).toString("base64") + `=?=
` + iconv.convert("nodemailerでISO-2022-JPのメールを送信したい( Mailparserじゃなくて送信)").toString()
};

smtpTransport.sendMail( message , function(error, r) {

これで送信すると、「Content-Type: text/plain; charset=ISO-2022-JP」で送信され、文字化けもなくクライアント側でもISO-2022-JPとして認識されて本文などが表示されます。

Twilio-PHP-SDK 4.xのdialを5.xにアップデートでDOMExceptionエラー(response->dialを5.xに対応させる)

TwilioPHP-SDKの4.xで書かれたこんなカンファレンスへの誘導と、完了後にsayするconference_complet.phpへのdial発信

着信のwebhookで起動します。

include './twilio-php/Services/Twilio.php';
include './twilio-php/Services/Twilio/Capability.php';

$response = new Services_Twilio_Twiml();
$dial = $response->dial(array(
	'action' => "https://***.com/conference_complet.php",
 	"method" => "GET",
));
$dial->conference("My Conference", array(
	"startConferenceOnEnter" => "true",
	"waitUrl" => "https://***.com/conference_wait.php",
 	"endConferenceOnExit" => "true",
 	"beep" => "true",
));
print $response;

dialの部分を下記のようにTwilio-PHP-SDK ver5.xで書いたのですが、print responseでXMLのDOMエラーが発生しました。

PHP Fatal error:  Method Twilio\\TwiML\\VoiceResponse::__toString() must not throw an exception, caught DOMException: Invalid Character Error 

conferenceは動くので、dialに問題があるようです。

$response = new VoiceResponse();

$dial = $response->dial([
	'action' => "https://***.com/conference_complet.php",
 	"method" => "GET",
));
print $response;

4.xで出力されるTwiMLは下記のようになっており、Dial要素の中はConferenceなので、dialにはactionなどのパラメータしか入力していないのですが、5.xではエラーになります

<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Dial action="https://***.com/conference_complet.php" method="GET">
<Conference startConferenceOnEnter="true" waitUrl="https://***.com/conference_wait.php" endConferenceOnExit="true" beep="true">My Conference</Conference>
</Dial>
</Response>

下記のようにdialの引数を変更すると、動作します。

$response = new VoiceResponse();

$dial = $response->dial("",[
	'action' => "https://***.com/conference_complet.php",
 	"method" => "GET",
));
print $response;

<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Dial action="https://***.com/conference_complet.php" method="GET">
<Conference startConferenceOnEnter="true" waitUrl="https://***.com/conference_wait.php" endConferenceOnExit="true" beep="true">My Conference</Conference>
</Dial>
</Response>

ITMS-90809: Deprecated API Usage (UIWebView APIを使用したアプリアップデート) 対策でGoogle-Mobile-Ads-SDKを更新

iOSアプリにちょっと修正を加えてアップロードしたら、こんなメッセージを受けました。

最新の配信で、1つ以上の問題が確認されました。配信は成功しましたが、次の配信では以下の問題を修正してください。ITMS-90809: Deprecated API Usage – Appleは2020年12月以降、UIWebView APIを使用したアプリアップデートの投稿を受け付けなくなります。詳細は https://developer.apple.com/documentation/uikit/uiwebview を参照してください。問題を修正したら、新しいバイナリをApp Store Connectにアップロードしてください。よろしくお願いします。App Store チーム

UIWebViewを利用していると表示されるのですが、アプリ本体はwkWebviewにしばらく前から移行してるので、何かのライブラリが原因のようです。

プロジェクトのディレクトリでfindしてみたらadmobのライブラリのようです。

Binary file ./Pods/Google-Mobile-Ads-SDK/Frameworks/frameworks/GoogleMobileAds.framework/GoogleMobileAds matches

admobのリリースノートにも、2020‑02‑28(7.56.0)で対応されてます。

https://developers.google.com/admob/ios/rel-notes

Warning: Calling this method may negatively impact your Google mediation performance. This method should only be called if you include Google mediation adapters in your app, but you won’t use mediate through Google during a particular app session (for example, you are running an A/B mediation test).

cocoapod経由でアップデートします。

pod update Google-Mobile-Ads-SDK
Updating local specs repositories

CocoaPods 1.8.4 is available.
To update use: `sudo gem install cocoapods`

For more information, see https://blog.cocoapods.org and the CHANGELOG for this version at https://github.com/CocoaPods/CocoaPods/releases/tag/1.8.4

Analyzing dependencies
Downloading dependencies
Installing Google-Mobile-Ads-SDK 7.57.0 (was 7.25.0)
Installing GoogleAppMeasurement (6.1.2)
Installing GoogleUtilities (6.5.2)
Generating Pods project
Integrating client project
Pod installation complete! There are 3 dependencies from the Podfile and 11 total pods installed.

アップデートしたところ、admob関連でアプリが落ちるので、調べてみると

*** Terminating app due to uncaught exception 'GADInvalidInitializationException', reason: 'The Google Mobile Ads SDK was initialized without an application ID. Google AdMob publishers, follow instructions here: https://googlemobileadssdk.page.link/admob-ios-update-plist to set GADApplicationIdentifier with a valid app ID. Google Ad Manager publishers, follow instructions here: https://googlemobileadssdk.page.link/ad-manager-ios-update-plist'
*** First throw call stack:
(
	0   CoreFoundation                      0x000000010a7f81ab __exceptionPreprocess + 171
	1   libobjc.A.dylib                     0x0000000109e8df41 objc_exception_throw + 48
	2   CoreFoundation                      0x000000010a7f80f9 -[NSException raise] + 9
	3   myapp02                      0x0000000104cffabb GADApplicationVerifyPublisherInitializedAnalyticsCorrectly + 414
	4   myapp02                      0x0000000104cb05d6 GADEnvironmentIsSupported + 1020
	5   libdispatch.dylib                   0x000000010da893f7 _dispatch_call_block_and_release + 12
	6   libdispatch.dylib                   0x000000010da8a43c _dispatch_client_callout + 8
	7   libdispatch.dylib                   0x000000010da8f352 _dispatch_queue_override_invoke + 1458
	8   libdispatch.dylib                   0x000000010da961f9 _dispatch_root_queue_drain + 772
	9   libdispatch.dylib                   0x000000010da95e97 _dispatch_worker_thread3 + 132
	10  libsystem_pthread.dylib             0x000000010df529f7 _pthread_wqthread + 220
	11  libsystem_pthread.dylib             0x000000010df51b77 start_wqthread + 15
)
libc++abi.dylib: terminating with uncaught exception of type NSException

Google Ad Managerで配信している場合に必要な設定が増えているようです。

info.plistにGADIsAdManagerAppのキーを作成して、YESを設定

Softether VPN Server のVPN(L2TP/IPsec)にMacから接続できない[サーバーとの接続が確立ができません]

リモートワークに伴い、事務所のPCへのリモートアクセスだけでは不便なので、VPN(L2TP/IPsec)のVPNサーバーを構築しました。

UbuntuのサーバーにSoftetherをインストールして、WindowsのVPN サーバー管理マネージャーから設定しました。

Mac用のVPN サーバー管理マネージャ ーもあるのですが、起動しない?ので急ぎでほしいので一旦Windowsで設定しました。

https://ja.softether.org/4-docs/2-howto/L2TP_IPsec_Setup_Guide/1

設定は完了しているのですが、macにVPNを設定して接続しても「サーバーとの接続が確立ができません」と出てしまうので、CUIから設定します。

# ./vpncmd
vpncmd command - SoftEther VPN Command Line Management Utility
SoftEther VPN Command Line Management Utility (vpncmd command)
Version 4.34 Build 9745   (English)
Compiled 2020/04/05 23:39:56 by buildsan at crosswin
Copyright (c) SoftEther VPN Project. All Rights Reserved.

By using vpncmd program, the following can be achieved.

1. Management of VPN Server or VPN Bridge
2. Management of VPN Client
3. Use of VPN Tools (certificate creation and Network Traffic Speed Test Tool)

Select 1, 2 or 3: 1

Specify the host name or IP address of the computer that the destination VPN Server or VPN Bridge is operating on.
By specifying according to the format 'host name:port number', you can also specify the port number.
(When the port number is unspecified, 443 is used.)
If nothing is input and the Enter key is pressed, the connection will be made to the port number 8888 of localhost (this computer).
Hostname of IP Address of Destination:未入力でエンター

If connecting to the server by Virtual Hub Admin Mode, please input the Virtual Hub name.
If connecting by server admin mode, please press Enter without inputting anything.
Specify Virtual Hub Name:未入力でエンター
Password: ********GUIのツールで入れた管理パスワード

VPN Server>HubList
HubList command - Get List of Virtual Hubs
Item              |Value
------------------+-------------------
Virtual Hub Name  |VPN ( GUIで作成したHubが表示される )
Status            |Online
Type              |Standalone
Users             |1
Groups            |0
Sessions          |0
MAC Tables        |0
IP Tables         |0
Num Logins        |0
Last Login        |2020-04-07 17:25:03
Last Communication|2020-04-07 17:25:04
Transfer Bytes    |7,077
Transfer Packets  |21
The command completed successfully.
VPN Server>UserList
UserList command - Get List of Users
Before executing this command, first select the Virtual Hub to manage using the Hub command.
VPN Server>HUB VPN ハブを選んでから
VPN Server/VPN>UserList
UserList command - Get List of Users
Item            |Value
----------------+-------------------------
User Name       |dp-user
Full Name       |dp-user
Group Name      |-
Description     |
Auth Method     |Password Authentication
Num Logins      |0
Last Login      |2020-04-07 (Tue) 17:25:03
Expiration Date |No Expiration
Transfer Bytes  |7,077
Transfer Packets|7,077
The command completed successfully.

VPN Server/VPN>IPsecEnable
IPsecEnable command - Enable or Disable IPsec VPN Server Function
Enable L2TP over IPsec Server Function (yes / no): yes

Enable Raw L2TP Server Function (yes / no): no

Enable EtherIP / L2TPv3 over IPsec Server Function (yes / no): no

Pre Shared Key for IPsec (Recommended: 9 letters at maximum): xxyba

Default Virtual HUB in a case of omitting the HUB on the Username: VPN

The command completed successfully.

VPN Server/VPN>
VPN Server/VPN>SecureNatEnable
SecureNatEnable command - Enable the Virtual NAT and DHCP Server Function (SecureNat Function)
The command completed successfully.

VPN Server/VPN>

最後のSecureNatEnableがキモで、これでMacから接続が出来るようになりました

UbuntuにSoftEther VPN Serverをインストールしようとしたら/runのサイズ不足エラー(Failed to reload daemon: Refusing to reload)

リモートワークで事務所にあるPCを踏み台にしていましたが、事務所のネットが安定しないので、念の為もう一台別でVPNサーバーを準備しようと思い、開発サーバーにしていたConohaVPSの512MBプランにインストールしましたが、systemctlのサービス登録でエラーがでました。

Failed to reload daemon: Refusing to reload, not enough space available on /run/systemd. Currently, 15.3M are free, but a safety buffer of 16.0M is enforced.

root@oreno-dev-server:/var# df -h
Filesystem      Size  Used Avail Use% Mounted on
udev            211M     0  211M   0% /dev
tmpfs            49M   33M   16M  69% /run
/dev/vda2        20G   11G  8.1G  57% /
tmpfs           241M     0  241M   0% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs           241M     0  241M   0% /sys/fs/cgroup
tmpfs            49M     0   49M   0% /run/user/0
/dev/loop1       92M   92M     0 100% /snap/core/8592
/dev/loop2       92M   92M     0 100% /snap/core/8689
root@crypt-webdb-dev:/var#

メインメモリが512Mなので動的に49Mなんですかね、fstabには指定がなかった。

root@oreno-dev-server:/var# ls -lah /tmp/
total 5.3M
drwxrwxrwt  9 root root 4.0K Apr  7 15:51 .
drwxr-xr-x 23 root root 4.0K Mar 18 06:35 ..
drwxrwxrwt  2 root root 4.0K Jun 20  2019 .ICE-unix
drwxrwxrwt  2 root root 4.0K Jun 20  2019 .Test-unix
drwxrwxrwt  2 root root 4.0K Jun 20  2019 .X11-unix
drwxrwxrwt  2 root root 4.0K Jun 20  2019 .XIM-unix
drwxrwxrwt  2 root root 4.0K Jun 20  2019 .font-unix
-rw-------  1 root root 2.7M Apr  7 14:33 .unicode_cache_225fcb7e.dat
-rw-------  1 root root 2.6M Apr  7 14:04 .unicode_cache_85d392aa.dat

tempfsのサイズ的にはデカいdatファイル”.unicode_cache”がある。ぐぐるとSoftEtherのキャッシュファイル?一旦削除してサービス登録だけ終わらせます。