スタイルシートをLESSにかえてみた。

サーバー側のCSS/JavaScriptをまとめて最適化+難読化するシステムを作り直して、The Dynamic Stylesheet language (LESS)に対応させました。
LESSのコンパイルにはlessphpライブラリを使用。

LESSってナニー?って感じですが、LESSは主に開発者側がCSSを使いやすく、メンテナンスが容易にするためのCSSプリプロセッサなんです。
CSSでネックだった変数、関数、演算が使えるので、開発者からすればCSSの視認性の上昇やコードの短縮ができます。

なのでWebを閲覧するユーザー側からしたら特にメリットはなく、クライアント側コンパイラを使う場合はページ表示速度低下があるのでデメリットしかありません。
しかし、コンパイルをサーバー側でやっておくとユーザー側のストレスはCSSと全く同じ。

これはメンテナンスをし易くするために使ってみねば!という事で・・・
とりあえず、よく使うアンカーエレメントの訪問済みリンクやマウスオーバーの色を指定するやつはこうなります。

CSS

a:link {
    color: #0d85cc;
    text-decoration: none;
}
 
a:visited {
    color: #0d85cc;
    text-decoration: none;;
}
 
a:active, a:hover {
    color: #12a7ff;
}
LESS

a {
    &:link {
        color: #0d85cc;
        text-decoration: none;
    }
 
    &:visited {
        color: #0d85cc;
        text-decoration: none;
    }
 
    &:active, &:hover {
        color: #12a7ff;
    }
}

 
少しややこしいCSSのサンプル。
このブログのフォームにも使っているCSSで、inputやtextareaのマウスオーバーやフォーカスを当てた時に縁が変化するやつはこうなる。
CSS

input, textarea, select {
    color: #666;
    background: #fff;
    font-size: 12px;
    line-height: 18px;
}
 
input:not([readonly]):not([disabled]):hover, 
textarea:not([readonly]):not([disabled]):hover, 
select:not([readonly]):not([disabled]):hover {
    background: #f6f6f6;
}
 
input:not([readonly]):not([disabled]):focus, 
textarea:not([readonly]):not([disabled]):focus, 
select:not([readonly]):not([disabled]):focus {
    transition-duration: 0.3s;
 
    background: #fff;
    border-color: #75b9f0;
    outline: 0px none;
    box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.075) inset, 0px 0px 8px #75b9f0;
}

LESS

input, textarea, select {
    color: #666;
    background: #fff;
    font-size: 12px;
    line-height: 18px;
 
    &:not([readonly]):not([disabled]):hover {
        background: #f6f6f6;
    }
 
    &:not([readonly]):not([disabled]):focus {
        transition-duration: 0.3s;
 
        background: #fff;
        border-color: #75b9f0;
        outline: 0px none;
        box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.075) inset, 0px 0px 8px #75b9f0;
    }
}

ロケーション疑似クラスを使ってたり複数指定しているCSSは格段に見やすく、そしてシンプルになりますねー!
 
うちのブログのCSSもLESSにかえてみたら、CSSで62.5Kb、LESSで37.7Kbのファイルサイズに。
LESSをコンパイルすれば同じぐらいのサイズになったので、書き方の違いがあるだけでCSSとLESSをコンパイルした物はかわらなかった。
CSSとLESS
画像はCSSをLESSに作り直しをしているところだけれど、視認性が高くファイルサイズが小さくなってるのがよくわかりますねw

本当にこれは使いやすくて良い感じ(`・ω・)b

WordPressのパスワード保護している記事のtitleも変更する

今現在、WordPressでパスワード保護された記事の本文やコメントは非表示になりますが、titleエレメントやRSSなんかは非表示になりません。
今回は非推奨になったwp_titleとWordPress 4.4から登場したadd_theme_support('title-tag')を有効化したテーマ等で使うwp_get_document_title()でも使える方法でいきます。

序でに「非公開:」や「保護中:」って文字も削除・変更しちゃいますー。

まずは完成したWordPressのプラグインコード。

<?php
/*
Plugin Name: Change Password POST Title
Plugin URI: http://blog.wolfs.jp/20160513-3616/
Description: パスワードで保護された記事のタイトルを変更します。
Version: 1.0.0
Author: Kerberos
Author URI: http://blog.wolfs.jp/
*/
 
class changePasswordPostTitle {
    private $plugin_dir = ABSPATH.'/wp-content/plugins/change_password_post_title/feed/';
    private $post_password_title = '[パスワードで保護された記事]';
 
    public function __construct() {
        add_filter('the_title', array($this, 'the_title'), 10, 2);
        add_filter('pre_get_document_title', array($this, 'pre_get_document_title'));
        add_filter('protected_title_format', array($this, 'protected_title_format'));
 
        add_filter('aioseop_title_single', array($this, 'aioseop_title'));
        add_filter('aioseop_title_page', array($this, 'aioseop_title'));
 
        remove_action('do_feed_rss2', 'do_feed_rss2');
        remove_action('do_feed_atom', 'do_feed_atom');
        add_action('do_feed_rss2', array($this, 'do_feed_rss2'), 10, 1);
        add_action('do_feed_atom', array($this, 'do_feed_atom'), 10, 1);
    }
 
 
    public function the_title($title = '', $currentID = 0) {
        global $post;
 
        if ($currentID !== 0) {
            if (post_password_required($currentID) === true) {
                if ($post->ID == $currentID && is_admin() === false) {
                    return $this->post_password_title;
                }
            }
 
            return $this->remove_title_prefix($title);
        }
 
        return $title;
    }
 
    public function pre_get_document_title($title) {
        if (is_single() === true || is_page() === true) {
            if (post_password_required($post->ID) === true) {
                return $this->post_password_title;
            }
 
            return $this->remove_title_prefix($title);
        }
 
        return $title;
    }
 
    public function protected_title_format($title) {
        global $post;
 
        if (post_password_required($post->ID) === false) {
            return '&#x1F512; %s';
        }
 
        return $this->post_password_title;
    }
 
    public function aioseop_title($title) {
        global $post;
 
        if (post_password_required($post->ID) === true) {
            return $this->post_password_title;
        }
 
        return $title;
    }
 
    private function remove_title_prefix($title) {
        $search[0] = '/^' . str_replace('%s', '(.*)', preg_quote(__('Protected: %s'), '/' )) . '$/';
        $search[1] = '/^' . str_replace('%s', '(.*)', preg_quote(__('Private: %s'), '/' )) . '$/';
 
        return preg_replace($search, '$1', $title);
    }
 
 
    public function do_feed_rss2($for_comments) {
        if ($for_comments) {
            load_template($this->plugin_dir.'feed-rss2-comments.php');
        }
    }
 
    public function do_feed_atom($for_comments) {
        if ($for_comments) {
            load_template($this->plugin_dir.'feed-atom-comments.php');
        }
    }
}
 
new changePasswordPostTitle();
?>

フィルターのフック名と関数は同じ名前にしてあるのでわかると思うので、その辺は略しますw
このプラグインでパスワード保護ページはこうなります:サンプルページ
続きを読む

e621.netで画像保存を楽にする

e621.netってケモノ絵や話題を投稿するサイトがあるんです。
ここは健全な絵もあるけれどR18な絵も多数ある所。
ケモノが好きな人は見てて飽きない所ですねw

ぼーっと見ていたら、とあるアーティストさんの過去絵が沢山あったので、ガンガン保存していたら保存ダイアログを出すのがメンドウになってきましたw
なので自動化・・はダルイしイラナイ画像も拾うから画像をダブルクリックするだけで保存ダイアログが出るようにuserChromeを作ってみました。

e621net-supporter.uc.js

// ==UserScript==
// @name           e621.net: 画像保存サポーター
// @namespace      http://blog.wolfs.jp/
// @description
// ==/UserScript==
 
var e621netSupporterObj = {
    targetHost: 'e621.net',
    targetPage: 'e621.net/post/show/',
 
    isTargetPage: false,
    lastTime: 0,
 
    clickID: {
        left: 0,
        right: 2,
        center: 1
    },
 
    init: function() {
        gBrowser.mPanelContainer.addEventListener("dblclick", this, true);
    },
 
    handleEvent: function(event) {
        if (gBrowser.currentURI.asciiSpec.indexOf(this.targetPage) != -1) {
            event = new XPCNativeWrapper(event);
            if (event.target instanceof HTMLImageElement) {
                if (event.target.id == 'image') {
                    event.preventDefault();
 
                    if (new Date().getTime() - this.lastTime > 5000) {
                        this.lastTime = new Date().getTime();
 
                        var img = this.getImage(event.target.src);
                        if (img != null) {
                            this.saveImage(img, this.getFileName(event.target.src));
                        }
                        img = null;
                    }
                }
            }
        }
    },
 
    getFileName: function(url) {
        return url.match(".+/(.+?)([\?#;].*)?$")[1];
    },
 
    getImage: function(url) {
        var request = new XMLHttpRequest();
        request.open('GET', url, false);
        request.overrideMimeType('text/plain; charset=x-user-defined');
        request.channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
        request.send(null);
 
        if (request.status == 200) {
            return request.responseText;
        }
 
        request = null;
        return null;
    },
 
    saveImage: function(imageData, requestFileName) {
        var fp = Components.classes["@mozilla.org/filepicker;1"].createInstance(Components.interfaces.nsIFilePicker);
        fp.init(window, 'Select a file', Components.interfaces.nsIFilePicker.modeSave);
        //fp.appendFilters(Components.interfaces.nsIFilePicker.filterImages);
        //fp.appendFilters(Components.interfaces.nsIFilePicker.filterAll);
        fp.appendFilter('JPEG Image', '*.jpg');
        fp.appendFilter('PNG Image', '*.png');
        fp.appendFilter('GIF Image', '*.gif');
        fp.appendFilters(Components.interfaces.nsIFilePicker.filterAll);
 
        fp.defaultString = requestFileName;
        switch (requestFileName.split('.')[1]) {
            case 'jpg':
                fp.defaultExtension = 'jpg';
                fp.filterIndex = 0;
                break;
            case 'png':
                fp.defaultExtension = 'png';
                fp.filterIndex = 1;
                break;
            case 'gif':
                fp.defaultExtension = 'gif';
                fp.filterIndex = 2;
                break;
            default:
                fp.filterIndex = 3;
        }
 
        switch (fp.show()) {
            case Components.interfaces.nsIFilePicker.returnOK:
            case Components.interfaces.nsIFilePicker.returnReplace:
                var outStream = Components.classes["@mozilla.org/network/safe-file-output-stream;1"].createInstance(Components.interfaces.nsIFileOutputStream);
                outStream.init(fp.file, 0x02 | 0x08 | 0x20, 0664, 0);
                outStream.write(imageData, imageData.length);
                if (outStream instanceof Components.interfaces.nsISafeOutputStream) {
                    outStream.finish();
                } else {
                    outStream.close();
                }
                outStream = null;
                break;
            case Components.interfaces.nsIFilePicker.returnCancel:
                break;
        }
    }
};
 
e621netSupporterObj.init();

本当はグリモンとかでサクッと実装したかったけれど、保存ダイアログをだすfilepickerやfile-output-streamなんかは呼び出せないので、権限があるuserChromeで実装。

内容は簡単。
 1.ダブルクリックされたらページのURLがe621.net/post/show/かどうか比較。
 2.対象のページだった場合はクリック先のエレメントがHTMLImageElementかつIDがimageか確認。
 3.imageだった場合はXMLHttpRequestで画像のバイナリを獲得。
   これは既に表示されている画像のキャッシュが効くのでいいb
 4.画像を正常に読み込めたら保存ダイアログを呼び出して保存先やファイル名を決定。
って感じ。

userChromeだけれど、内容はFirefox系のAPIに依存しているのでFirefox系ブラウザ専用です。

アーカイブ拒否を無視するarchive.isをブロックする

Webサイトを保存するオンラインサービスは幾つもありますが、大抵はrobotsへnoarchive属性を付けておけば保存されません。
それを無視するサービスでも削除要求や「今後一切アーカイブしないでくれ」と行った要求が通るのでちょっと面倒なだけでした。

今回なぜarchive.isを知ってブロックする事になったかと言うと・・
別件で過去3年分のサーバーアクセスログからリンク元一覧を作成していたら「archive.is」からのリンクを発見。
なにかなー?とリンク元をゲットしてみると、対象URLのデータ丸ごとアーカイブされていました。

コードを見てみると、GoogleAnalytics関連は無効化されているので余計なログは残らないしGoogleのポリシーにも違反しないのは良いところだけれど・・・
元ページなんかを示すcanonicalタグは書き換えられていました。
まあどのサイトをキャッシュしたかはキャッシュページに記載されているから「どのサイトかわからない」って事はないけれど、重要なcanonicalを削除するのはひどいなあ。

うちのブログは有料素材も使っているし、なにより自分の死後にデータを残したくないのでarchive.isへ削除要求を送りましたが無視され続けました。
これはもうサーバー側でブロックするしかないって事で、ダミーURLにarchive.isのクローラーを差し向けて色々情報を頂いちゃうことに。
(ブロックに必要なIPアドレスのみ必要な方は記事の最後へ)
専用ページもできました。

まずはアクセスログ。
ちょっと書式を変更してあるので普通とは違うところがありますが、概ねApache標準の書式です。

46.166.139.173 blog.wolfs.jp - [22/Apr/2016:04:18:00 +0900] "GET /XIHI2TmvFBCoYOL6M4JFZkGfvCLn6Q5H HTTP/1.1" 404 49368 "https://www.google.com/" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"
46.166.139.173 sblog.wolfs.jp - [22/Apr/2016:04:18:01 +0900] "GET /optimizer.css?theme_p,hlstring,slimbox2 HTTP/1.1" 200 8345 "http://blog.wolfs.jp/XIHI2TmvFBCoYOL6M4JFZkGfvCLn6Q5H" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"
46.166.139.173 sblog.wolfs.jp - [22/Apr/2016:04:18:01 +0900] "GET /wp-includes/js/jquery/jquery.js?ver=1.11.3 HTTP/1.1" 200 33267 "http://blog.wolfs.jp/XIHI2TmvFBCoYOL6M4JFZkGfvCLn6Q5H" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"
46.166.139.173 sblog.wolfs.jp - [22/Apr/2016:04:18:01 +0900] "GET /optimizer.js?theme_p,captcha,slimbox2,lazy-load,jwplayer,jwplayer_after,akismet,attachment HTTP/1.1" 200 52725 "http://blog.wolfs.jp/XIHI2TmvFBCoYOL6M4JFZkGfvCLn6Q5H" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"
46.166.139.173 sblog.wolfs.jp - [22/Apr/2016:04:18:02 +0900] "GET /themes/kerberos/images/header.jpg HTTP/1.1" 200 75529 "http://blog.wolfs.jp/XIHI2TmvFBCoYOL6M4JFZkGfvCLn6Q5H" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"
46.166.139.173 sblog.wolfs.jp - [22/Apr/2016:04:18:02 +0900] "GET /themes/kerberos/images/sprite.png HTTP/1.1" 200 8803 "http://blog.wolfs.jp/XIHI2TmvFBCoYOL6M4JFZkGfvCLn6Q5H" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"
46.166.139.173 sblog.wolfs.jp - [22/Apr/2016:04:18:02 +0900] "GET /themes/kerberos/images/paw.jpg HTTP/1.1" 200 16631 "http://blog.wolfs.jp/XIHI2TmvFBCoYOL6M4JFZkGfvCLn6Q5H" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"
46.166.139.173 sblog.wolfs.jp - [22/Apr/2016:04:18:02 +0900] "GET /themes/kerberos/images/hidewolf.png HTTP/1.1" 200 5370 "http://blog.wolfs.jp/XIHI2TmvFBCoYOL6M4JFZkGfvCLn6Q5H" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"
46.166.139.173 sblog.wolfs.jp - [22/Apr/2016:04:18:09 +0900] "GET /plugins/slimbox/css/closelabel.gif HTTP/1.1" 200 971 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.5.1 (development) Safari/534.34"
46.166.139.173 sblog.wolfs.jp - [22/Apr/2016:04:18:09 +0900] "GET /plugins/slimbox/css/closelabel.gif HTTP/1.1" 200 971 "http://blog.wolfs.jp/XIHI2TmvFBCoYOL6M4JFZkGfvCLn6Q5H" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"

おっと・・対象URLのコンテンツに対してはリファラー偽装してアクセスしてきています。
こうなるとユーザーエージェントのWindows7とChromeも怪しいところ。

しかし、Slimboxのgifに対してのみPhantomJSのユーザーエージェントが入っていました。
PhantomJSはコンソールベースのブラウザでwebkit系をエミュレートできるようです。
archive.isはこれを使ってデータをアーカイブしているんでしょうね。

次はサーバーのIPアドレスをRIRからいただきます。

% This is the RIPE Database query service.
% The objects are in RPSL format.
%
% The RIPE Database is subject to Terms and Conditions.
% See http://www.ripe.net/db/support/db-terms-conditions.pdf
% Note: this output has been filtered.
% To receive output for a database update, use the "-B" flag.
% Information related to '46.166.139.0 - 46.166.139.255'
% Abuse contact for '46.166.139.0 - 46.166.139.255' is 'abuse@nforce.com'
inetnum: 46.166.139.0 - 46.166.139.255
netname: NFORCE_ENTERTAINMENT
descr: Serverhosting
org: ORG-NE3-RIPE
country: NL
admin-c: NFAR
tech-c: NFTR
status: ASSIGNED PA
mnt-by: MNT-NFORCE
mnt-lower: MNT-NFORCE
mnt-routes: MNT-NFORCE
created: 2015-06-05T22:10:53Z
last-modified: 2015-06-05T22:10:53Z
source: RIPE # Filtered
remarks: INFRA-AW
organisation: ORG-NE3-RIPE
org-name: NForce Entertainment B.V.
org-type: LIR
address: Postbus 1142
address: 4700BC
address: Roosendaal
address: NETHERLANDS
phone: +31206919299
fax-no: +31206919409
abuse-mailbox: abuse@nforce.com
admin-c: PT3315-RIPE
admin-c: JVDM119-RIPE
admin-c: JH24522-RIPE
admin-c: DI1505-RIPE
admin-c: NFAR
tech-c: NFTR
mnt-ref: MNT-NFORCE
mnt-ref: RIPE-NCC-HM-MNT
mnt-ref: MNT-NFORCE
mnt-by: RIPE-NCC-HM-MNT
abuse-c: NFAB
created: 2007-06-19T08:39:06Z
last-modified: 2015-03-27T11:27:05Z
source: RIPE # Filtered

person: NFOrce Entertainment BV - Administrative role account
address: Postbus 1142
address: 4700BC Roosendaal
address: The Netherlands
phone: +31 (0)206919299
fax-no: +31 (0)206919409
abuse-mailbox: abuse@nforce.com
nic-hdl: NFAR
mnt-by: MNT-NFORCE
created: 2010-11-13T14:42:50Z
last-modified: 2013-05-15T07:49:25Z
source: RIPE # Filtered

person: NFOrce Entertainment BV - Technical role account
address: Postbus 1142
address: 4700BC Roosendaal
address: The Netherlands
phone: +31 (0)206919299
fax-no: +31 (0)206919409
abuse-mailbox: abuse@nforce.com
nic-hdl: NFTR
mnt-by: MNT-NFORCE
created: 2010-11-13T14:43:05Z
last-modified: 2013-05-15T07:50:27Z
source: RIPE # Filtered

% Information related to '46.166.136.0/21AS43350'
route: 46.166.136.0/21
descr: NFOrce Entertainment BV - route 46.166.136.0/21
origin: AS43350
mnt-by: MNT-NFORCE
created: 2014-10-10T12:35:38Z
last-modified: 2014-10-10T12:35:38Z
source: RIPE
% This query was served by the RIPE Database Query Service version 1.86 (DB-2)

サーバーはオランダにあるNForce Entertainment B.V.って会社のようです。
サーバー自体は普通のホスティングサーバーなのでアビューズ報告もダメでしょうし、archive.isに割り当てられたIP一覧も答えてくれないでしょう。

とりあえず今回はarchive.isのIPアドレスを含む46.166.136.0/21をブロックする事に。
NForce Entertainment B.V.の全IPアドレスはこちら:http://ipinfo.io/AS43350

続きを読む

Creative プロダクト レジストレーションの削除

2008年に買った Sound Blaster X-Fi Titanium Professional Audioがお亡くなりになったので、ドライバーや制御ソフトをアンインストールしていたら「Creative プロダクト レジストレーション」ってソフトだけが残っちゃいました。
登録情報エントリを見ても登録されておらず、Program Filesのプログラムフォルダにはアンインストーラーも存在しない状態。

Creativeのサポートに連絡したところ1時間ほどの連絡の末「手動で対象フォルダを削除してくれ」と言うことでした。
どうもCreativeはプロダクト レジストレーションのアンインストールは想定していないようで、対応も不明点が多かったので自分で対象フォルダを調べてみる。
対象フォルダは「C:\Program Files (x86)\Creative」
「Creative」フォルダ
中を見てみるとSFBM、Shared Files、プロダクト レジストレーションのフォルダが。
Shared Filesの中にはCTRegSvr.exeを始めAudio.pid等のファイルがあり、PIDファイルの中身は全てPID pluginってDLLファイルでした。
regsvr32にかけてみたけれど登録はされていないよう。

プロダクト レジストレーションフォルダを開くとJapaneseフォルダ、CtCrypto.dll、CtORWebClient.ocxがありました。
「プロダクト レジストレーション」フォルダ
CtORWebClient.ocxはIEのActiveXプラグインなのでファイル削除だけでは完全にアンインストール不可能。
管理権限のあるコマンドプロンプトで「regsvr32 /u CtORWebClient.ocx」を実行。
regsvr32 /u CtORWebClient.ocx
これでCtORWebClient.ocxアンインストール完了
CtCrypto.dllは専用のDLLではないとの表記がでるのでそのままでOK。

Japaneseフォルダの中身は登録するためのプログラム本体がある場所のよう。
「プロダクト レジストレーション/Japanese」フォルダ
ショートカットは同フォルダのInetReg.exeへ引数/PreProcess=RegFlash.exeを付け加えた物でした。
iniを見ても特になにもなかったし他に登録されている物もないので、ここで何かをする事は必要なさそう。

あとは「C:\Program Files (x86)\Creative」、「C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Creative」を削除して終了。
Google Updateもそうだけど関連ソフトが全部アンインストールされたら一緒にアンインストールされるようにして欲しいところ。