0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Android】detektで共通Wrapper以外からの直接呼び出しをビルドエラーにする

0
Posted at

はじめに

近年、国内でも個人情報や認証情報の漏洩に関する報道が続いています。原因は高度な攻撃だけとは限りません。アプリ開発の現場では、実装上の「うっかり」が漏洩の入口になることもあります。

  • デバッグ用に出力したログに、メールアドレスやトークンが残ってしまう
  • 暗号化されていない形でローカルに保存してしまう
  • 証明書ピンニングなどの共通対策を通さない通信を、誰かが直接書いてしまう

アプリ開発者が意識すべき要求も増えています。たとえば個人情報保護法の安全管理措置や、利用者情報を外部に送信する際の電気通信事業法の外部送信規律があります(法的な解釈は、各社の法務・セキュリティ部門にご確認ください)。

そこで多くのチームは「ログは必ず共通の AppLogger を通す」「通信は共通のHTTPクライアントを使う」といった共通Wrapperクラスを用意します。しかし、Wrapperを用意しても、ドキュメントやコードレビューだけでは直接呼び出しを防げません。新しく入ったメンバーが Log.d を書き、レビューをすり抜けてしまうのは時間の問題です。

本記事では、「ルールを文書ではなくビルドで強制する」ために、detekt を使って Wrapper 以外からの直接呼び出しをビルド失敗にする方法を紹介します。実際に動かして確認した結果も載せています。

  • import を検出して禁止する(基本)
  • import を使わず、パッケージ名を全部書いた直接呼び出し(完全修飾名)も禁止する
  • detekt だけでは防げない抜け道と、その塞ぎ方

動作確認環境: detekt-cli 1.23.8(JDK 21)。型解決は CLI の --classpath で指定して確認しました。Gradle プラグイン経由の設定は公式ドキュメントに基づいており、実際の Android プロジェクトでは動かしていません。

結論を先に

防御は1つの手段に頼らず、3層で組みます。

層 手段 防げるもの 弱点
1 Gradleモジュール分離(implementation) サードパーティライブラリ(OkHttpなど)への直接参照を本物のコンパイルエラーにできる android.util.Log のような Android 標準APIは classpath から消せない
2 detekt ForbiddenImport import による参照(別名importも) ワイルドカードimportや、importなしの完全修飾名は見逃す
2 detekt ForbiddenMethodCall(型解決あり) 完全修飾名の直接呼び出し、関数参照、コンストラクタ、サブクラス経由の呼び出し 型だけの参照や @Suppress による回避は防げない
3 grepゲート(CI) 型参照、Java、文字列、@Suppress コメント内の記述も拾う(誤検知)

なお、detekt は Kotlin コンパイラそのものではないため、厳密には「コンパイルエラー」ではなく「check タスクの失敗(ビルド失敗)」になります。本物のコンパイルエラーにできるのは第1層(モジュール分離)です。

最大の落とし穴: Gradle の check に自動で組み込まれるのは、型解決なしの detekt タスクだけです(公式ドキュメント)。完全修飾名の検出に必要な ForbiddenMethodCall は型解決が必須なので、そのままだと黙って素通りします。後述の「check に型解決タスクを組み込む」まで設定して初めて、本記事の対策が成立します。

題材: 共通Wrapperと、禁止したい直接呼び出し

以下の3つを例に進めます。

禁止する直接呼び出し 使うべきWrapper Wrapperで一箇所に集約する対策
android.util.Log / println AppLogger リリースビルドでは出力しない、個人情報をマスクする
OkHttpClient / OkHttpClient.Builder の直接生成 SecureHttpClientFactory 証明書ピンニング、平文HTTPの禁止、共通ヘッダー
Context.getSharedPreferences SecureStorage 暗号化して保存する

Wrapperは com.example.app.core.security.wrapper パッケージに置く前提です。

package com.example.app.core.security.wrapper

import android.util.Log

object AppLogger {
    /** Application.onCreate で BuildConfig.DEBUG などを設定する */
    @Volatile
    var enabled: Boolean = false

    fun d(tag: String, message: String) {
        if (enabled) Log.d(tag, PiiMasker.mask(message))
    }

    fun e(tag: String, message: String, throwable: Throwable? = null) {
        if (enabled) Log.e(tag, PiiMasker.mask(message), throwable)
    }
}

internal object PiiMasker {
    private val email = Regex("""[\w.+-]+@[\w-]+\.[\w.-]+""")
    fun mask(text: String): String = email.replace(text, "***@***")
}
package com.example.app.core.security.wrapper

import okhttp3.CertificatePinner
import okhttp3.ConnectionSpec
import okhttp3.OkHttpClient

object SecureHttpClientFactory {
    private val pinner = CertificatePinner.Builder()
        .add("api.example.com", "sha256/<ピン留めするハッシュ>")
        .build()

    fun create(): OkHttpClient = OkHttpClient.Builder()
        .connectionSpecs(listOf(ConnectionSpec.MODERN_TLS)) // 平文HTTPを許可しない
        .certificatePinner(pinner)
        .build()
}
package com.example.app.core.security.wrapper

import android.content.Context
import android.content.SharedPreferences

class SecureStorage(private val context: Context) {
    private val prefs: SharedPreferences
        get() = context.getSharedPreferences("secure_prefs", Context.MODE_PRIVATE)

    fun putString(key: String, value: String) {
        // 実際の実装では EncryptedSharedPreferences などで暗号化して保存する
        prefs.edit().putString(key, value).apply()
    }
}

Wrapper の内部では、当然 Log や OkHttpClient.Builder、getSharedPreferences を直接使います。この Wrapper だけを禁止ルールの例外にするのが、以降の設計の肝です。

第1層: モジュール分離で、本物のコンパイルエラーにする

OkHttp のようなサードパーティライブラリは、依存関係の宣言の仕方で「そもそも他のモジュールから見えなくする」ことができます。

// core/security-wrapper/build.gradle.kts
dependencies {
    // api ではなく implementation にする
    implementation("com.squareup.okhttp3:okhttp:<バージョン>")
}

implementation で宣言した依存は、このモジュールを利用する側のコンパイル用クラスパスには含まれません。そのため app モジュールで import okhttp3.OkHttpClient と書くと、Unresolved reference という本物のコンパイルエラーになります。

ただし、次の2点に注意が必要です。

  • android.util.Log や Context.getSharedPreferences のような Android 標準APIは、どのモジュールからも見えるため、この方法では防げません。ここから先の detekt が必要になります。
  • 他のモジュールが同じライブラリを自分で依存宣言すると見えてしまいます。依存宣言の追加も、レビュー対象にしてください。

第2層: detekt で import と完全修飾名の直接呼び出しを検知する

導入

// build.gradle.kts(各モジュールに適用。subprojects や convention plugin 経由でも可)
plugins {
    id("io.gitlab.arturbosch.detekt") version "1.23.8"
}

detekt {
    buildUponDefaultConfig = true
    config.setFrom(rootProject.file("config/detekt/detekt.yml"))
}

バージョンについて: 公式ドキュメントでは、1.23.8 は「もうアクティブにはメンテナンスされていない」とされ、最新版は 2.0.0-alpha 系(プラグインIDは dev.detekt)と案内されています。2.0系では ForbiddenImport の設定キーが forbiddenImports(理由 reason を付けられる)と allowedImports に変わります。本記事の設定は 1.23.8 のものです。2.0系を使う場合は読み替えてください。

設定: detekt.yml

build:
  maxIssues: 0   # 既定値だが、指摘があればビルドを失敗させる意図を明示する

style:
  # ① ワイルドカードimportを禁止する(後述: ForbiddenImportはワイルドカードを見逃すため)
  WildcardImport:
    active: true
    excludeImports: []

  # ② import を禁止する(基本)
  ForbiddenImport:
    active: true
    imports:
      - 'android.util.Log'
    forbiddenPatterns: '^okhttp3\..*'   # パッケージごと禁止したい場合は正規表現
    excludes: ['**/core/security/wrapper/**']

  # ③ import を使わない完全修飾名の直接呼び出しも禁止する(型解決が必須)
  ForbiddenMethodCall:
    active: true
    methods:
      # ログ(名前だけ指定すると同名の呼び出しがすべて対象になる)
      - 'android.util.Log.v'
      - 'android.util.Log.d'
      - 'android.util.Log.i'
      - 'android.util.Log.w'
      - 'android.util.Log.e'
      - 'android.util.Log.wtf'
      - 'kotlin.io.println'
      - 'kotlin.io.print'
      # 通信(コンストラクタは <init> と書く)
      - 'okhttp3.OkHttpClient.<init>'
      - 'okhttp3.OkHttpClient.Builder.<init>'
      # ローカル保存
      - 'android.content.Context.getSharedPreferences'
    excludes: ['**/core/security/wrapper/**']

ポイントは次の通りです。

  • ForbiddenImport は imports(完全一致のリスト)と forbiddenPatterns(正規表現)の2通りで指定できます。
  • ForbiddenMethodCall は、android.util.Log.d のように名前だけを書くと同名の呼び出しがすべて対象になります。java.util.Date.<init> のようにコンストラクタも指定できます。
  • excludes はルール単位のファイルパス(グロブ)による除外で、Wrapper の中だけを例外にします。

実際に何が検知されるか

次のようなコードを用意して、型解決あり・なしで実行しました。android.util.Log などは、検証用のスタブクラスに置き換えています。

// ① 通常のimport
import android.util.Log
fun a() { Log.d("TAG", "user=taro@example.com") }

// ② 別名import
import android.util.Log as L
fun b() { L.d("TAG", "alias") }

// ③ ワイルドカードimport
import android.util.*
fun c() { Log.d("TAG", "wildcard") }

// ④ importなしの完全修飾名
fun d() { android.util.Log.d("TAG", "fully qualified") }

// ⑤ 完全修飾名の関数参照
fun e() { val f = android.util.Log::d; f("T", "ref") }
パターン ForbiddenImport ForbiddenMethodCall(型解決あり) WildcardImport
① 通常のimport 検知 検知 ―
② 別名import 検知 検知 ―
③ ワイルドカードimport 見逃す 検知 検知
④ importなしの完全修飾名 見逃す 検知 ―
⑤ 完全修飾名の関数参照 見逃す 検知 ―
okhttp3.OkHttpClient.Builder() を完全修飾名で生成 見逃す 検知 ―
Activity のサブクラスから getSharedPreferences ― 検知(Context を指定すれば、オーバーライド経由でも検知) ―

型解決なしで実行すると、ForbiddenMethodCall は何も報告せず素通りしました(上表の「検知」は型解決ありの場合のみ)。import 行は ForbiddenImport が拾えますが、④⑤のような完全修飾名の直接呼び出しはすり抜けます。型解決ありで実行したときの出力は次の通りです(CLI の出力、パスは省略)。

BadImport.kt:3:1: The import `android.util.Log` has been forbidden in the detekt config. [ForbiddenImport]
BadImport.kt:6:21: The method `android.util.Log.d` has been forbidden in the detekt config. [ForbiddenMethodCall]
BadFqn.kt:4:34: The method `android.util.Log.d` has been forbidden in the detekt config. [ForbiddenMethodCall]
BadFqn.kt:5:43: The method `android.util.Log.d` has been forbidden in the detekt config. [ForbiddenMethodCall]
BadWildcard.kt:3:1: android.util.* is a wildcard import. Replace it with fully qualified imports. [WildcardImport]
Analysis failed with 8 weighted issues.

指摘があると終了コードは 2 になり、ビルドが失敗します。Wrapper 内(excludes 対象)の Log や android.util.Log.d は、import でも完全修飾名でも指摘されませんでした。

check に型解決タスクを組み込む

前述の通り、check に自動で含まれるのは型解決なしの detekt だけです。ForbiddenMethodCall を効かせるには、型解決つきのタスクを明示的に check に組み込みます。

// Androidモジュールの場合: バリアント名つきのタスクが生成される
tasks.named("check") {
    dependsOn("detektDebug")
}

JVM モジュールなら detektMain です。Android では detekt<Variant>(detektDebug、detektRelease など)が型解決つきのタスクです。CI では、./gradlew detektDebug を必須チェックとして明示的に実行しておくと安全です。

ローカルのビルド(Android Studio の Run)でも即座に落としたい場合は、preBuild に依存させる方法もあります。ただし、環境によっては実行時間や依存関係の問題が出る可能性があり、私は検証していません。まずは check と CI で確実に止めるところから始めるのがおすすめです。

Wrapper の除外は、緩いパターンにしない

# 悪い例: 誰かが "wrapper" という名前のパッケージを作れば抜けられる
excludes: ['**/wrapper/**']

# 良い例: フルパスで限定する
excludes: ['**/core/security/wrapper/**']

例外にしたパスは、後述の CODEOWNERS で守ります。

NG例とOK例: こう呼ぶとビルドエラー、この実装なら通る

ここまでの設定(第1層・第2層)を前提に、呼び出し方ごとの結果を整理します。ここでいう「ビルドエラー」は、detekt が指摘して check が失敗することを指します。

以下はすべて、上記の detekt.yml と Wrapper(core/security/wrapper/ 配下)で実際に実行した結果です(型解決あり)。出力はパスを省略しています。

NG: こう呼ぶとビルドエラーになる

NG-1: Log を import して呼ぶ

import android.util.Log

class NgLog {
    fun onLogin(email: String) {
        Log.d("Login", "email=$email")
    }
}
NgLog.kt:3:1: The import `android.util.Log` has been forbidden in the detekt config. [ForbiddenImport]
NgLog.kt:7:13: The method `android.util.Log.d` has been forbidden in the detekt config. [ForbiddenMethodCall]

import と呼び出しの両方が指摘されます。

NG-2: import せず、パッケージ名を全部書いて呼ぶ

class NgLogFqn {
    fun onLogin(email: String) {
        android.util.Log.d("Login", "email=$email")
    }
}
NgLogFqn.kt:5:26: The method `android.util.Log.d` has been forbidden in the detekt config. [ForbiddenMethodCall]

import がないため ForbiddenImport では検知できませんが、ForbiddenMethodCall が捕まえます。

NG-3: println で出力する

class NgPrintln {
    fun onLoaded(count: Int) {
        println("count=$count")
    }
}
NgPrintln.kt:5:9: The method `kotlin.io.println` has been forbidden in the detekt config. [ForbiddenMethodCall]

NG-4: OkHttp を import して直接生成する

import okhttp3.OkHttpClient

class NgHttp {
    val client: OkHttpClient = OkHttpClient.Builder().build()
}
NgHttp.kt:3:1: The import `okhttp3.OkHttpClient` has been forbidden in the detekt config. [ForbiddenImport]
NgHttp.kt:6:45: The method `okhttp3.OkHttpClient.Builder.<init>` has been forbidden in the detekt config. [ForbiddenMethodCall]

NG-5: OkHttp を import せず、パッケージ名を全部書いて生成する

class NgHttpFqn {
    val client: okhttp3.OkHttpClient = okhttp3.OkHttpClient()
}
NgHttpFqn.kt:4:48: The method `okhttp3.OkHttpClient.<init>` has been forbidden in the detekt config. [ForbiddenMethodCall]

NG-6: Activity の中で getSharedPreferences を直接呼ぶ

import android.app.Activity

class NgPrefs : Activity() {
    fun save() {
        getSharedPreferences("user", MODE_PRIVATE)
    }
}
NgPrefs.kt:7:9: The method `android.content.Context.getSharedPreferences` has been forbidden in the detekt config. [ForbiddenMethodCall]

Activity は Context のサブクラスですが、Context.getSharedPreferences を指定しておけば、サブクラス経由の呼び出しも検知されます。

OK: この実装ならビルドエラーにならない

NG の呼び出しを、Wrapper 経由に置き換えると通ります。

NG(直接呼び出し) OK(Wrapper経由)
Log.d("Login", "email=$email") AppLogger.d("Login", "email=$email")
println("count=$count") AppLogger.d("List", "count=$count")
OkHttpClient.Builder().build() SecureHttpClientFactory.create()
getSharedPreferences("user", MODE_PRIVATE) SecureStorage(context).putString(key, value)

OK-1: ログは AppLogger を通す

import com.example.app.core.security.wrapper.AppLogger

class OkLog {
    fun onLogin(email: String) {
        AppLogger.d("Login", "email=$email")
    }
}

OK-2: HTTPクライアントは SecureHttpClientFactory から取得する

import com.example.app.core.security.wrapper.SecureHttpClientFactory

class OkHttp {
    val client = SecureHttpClientFactory.create()
}

OK-3: ローカル保存は SecureStorage を通す

import android.content.Context
import com.example.app.core.security.wrapper.SecureStorage

class OkStorage(context: Context) {
    private val storage = SecureStorage(context)

    fun save(token: String) = storage.putString("token", token)
}

上の3つは、指摘ゼロで終了コード0でした。

OK-4: Wrapper の内部では、直接呼び出しても指摘されない

AppLogger の中の Log.d(...)、SecureHttpClientFactory の中の OkHttpClient.Builder()、SecureStorage の中の getSharedPreferences(...) は、excludes の対象パス(core/security/wrapper/)にあるため指摘されません。同じコードをこのパスの外にコピーすると、NG になります。

OK-5: 自作クラスのメソッド名が d や println でも指摘されない

class MyLogger {
    private val lines = mutableListOf<String>()

    fun d(tag: String, message: String) {
        lines += "$tag: $message"
    }

    fun println(message: String) {
        lines += message
    }
}

class OkNameCollision {
    private val logger = MyLogger()

    fun run() {
        logger.d("TAG", "ok")
        logger.println("ok")
    }
}

ForbiddenMethodCall は、メソッド名の文字列ではなく型解決で特定した完全修飾名で判定します。自作の MyLogger.d や MyLogger.println は、android.util.Log.d や kotlin.io.println とは別物なので、誤検知されません(検証済み)。これも、型解決が必須である理由のひとつです。

なお、この OK-5 の MyLogger は「検知されない」だけで、「ログを一元管理する」という Wrapper の目的は果たしません。直接呼び出しの禁止は、あくまで抜け道を塞ぐための仕組みです。ログの出力先やマスク処理といった実際の対策は、Wrapper 側に実装してください。

第3層: detekt で防げない抜け道を、grep で塞ぐ

detekt は便利ですが、次の抜け道は防げません。いずれも実際に確認しました。

// 型参照だけで、メソッド呼び出しではない
fun typeOnly(x: android.util.Log?) = x
fun klass() = android.util.Log::class
// ファイル全体で検知を無効化する
@file:Suppress("ForbiddenImport", "ForbiddenMethodCall")
package com.example.app.sup

import android.util.Log

class Suppressed { fun run() { Log.d("T", "bypass?") } }
  • ForbiddenMethodCall はメソッド呼び出しとコンストラクタが対象で、型の参照は検知しません。
  • @Suppress を付けると、detekt は完全に黙ります(検証では指摘ゼロ、終了コード0)。セキュリティ目的では、これが最も大きな穴になります。
  • detekt は Kotlin しか見ません。Java のコードは対象外です。

そこで、ソースをテキストとして検索する単純なゲートを CI に追加します。detekt の仕組みに依存しないので、@Suppress の影響を受けません。

#!/usr/bin/env bash
# usage: forbidden-grep.sh <src-root> <許可するパスの正規表現>
set -u
ROOT="$1"; ALLOW="$2"
PATTERN='android\.util\.Log|okhttp3\.|android\.content\.SharedPreferences|Suppress\([^)]*Forbidden'
hits=$(grep -rnE "$PATTERN" --include='*.kt' --include='*.java' "$ROOT" | grep -vE "$ALLOW" || true)
if [ -n "$hits" ]; then echo "$hits"; exit 1; fi
./forbidden-grep.sh app/src 'core/security/wrapper/'

検証用のコードに対して実行した結果、型参照のみ(x: android.util.Log?、Log::class)や @Suppress を使ったファイルも含めて検知できました(抜粋)。許可したパスだけを対象にした場合は、何も出力されず終了コード0になります。

src/.../ui/BadFqn.kt:4:    fun run() { android.util.Log.d("TAG", "fully qualified, no import") }
src/.../data/Gap.kt:6:    fun typeOnly(x: android.util.Log?) = x
src/.../data/Gap.kt:7:    fun klass() = android.util.Log::class
src/.../sup/Suppressed.kt:1:@file:Suppress("ForbiddenImport", "ForbiddenMethodCall")
src/.../sup/Suppressed.kt:4:import android.util.Log

Gradle タスクとして書く場合の例です(こちらは動かして確認していません)。

val forbiddenRegex = Regex("""android\.util\.Log|okhttp3\.|android\.content\.SharedPreferences|Suppress\([^)]*Forbidden""")
val baseDir = projectDir

tasks.register("checkForbiddenApi") {
    group = "verification"
    val sources = fileTree("src") {
        include("**/*.kt", "**/*.java")
        exclude("**/core/security/wrapper/**")
    }
    inputs.files(sources)
    doLast {
        val violations = sources.files.flatMap { file ->
            file.readLines().mapIndexedNotNull { index, line ->
                if (forbiddenRegex.containsMatchIn(line)) {
                    "${file.relativeTo(baseDir)}:${index + 1}: ${line.trim()}"
                } else null
            }
        }
        if (violations.isNotEmpty()) {
            throw GradleException("禁止APIの直接参照を検出しました:\n" + violations.joinToString("\n"))
        }
    }
}

tasks.named("check") { dependsOn("checkForbiddenApi") }

コメント内の言及も拾うため誤検知は出ますが、セキュリティ目的の最後のゲートとしては、「厳しめに倒す」方が安全です。

運用で気をつけること

既存の違反は baseline に寄せ、新規だけ必ず落とす

既存コードに大量の違反がある場合は、detekt の baseline 機能で既存分を一旦除外し、新規の違反だけを失敗させる運用が現実的です。ただし、baseline に入れた違反は「見えなくなった負債」なので、件数を記録して計画的に Wrapper へ移行してください。

設定ファイルと Wrapper を CODEOWNERS で守る

ルールを守らせる仕組みも、書き換えられては意味がありません。次の変更は、セキュリティ担当などの承認を必須にします。

  • config/detekt/detekt.yml(禁止リストと excludes)
  • core/security/wrapper/ 配下(信頼境界そのもの)
  • 依存関係の追加(build.gradle.kts)
  • @Suppress("Forbidden...") を含む差分(第3層のgrepが検知)

テストコードの扱いを決めておく

テストコードで println などを使いたい場合は、excludes に **/test/** を加えるかどうかを、チームで明確に決めてください。

型解決つきの解析は、通常より時間がかかる傾向がある

一般に、型解決つきの解析は通常の detekt より時間がかかります。CI では必須チェックとして実行し、ローカルでは必要なときに ./gradlew detektDebug を実行する運用が扱いやすいです。

限界

最後に、この仕組みで防げないことも整理しておきます。

  • リフレクションや動的ロード: Class.forName("android.util.Log") のように文字列が固定なら grep で拾えますが、動的に組み立てられると検知できません。
  • Wrapper そのものの不備: Wrapper の中に脆弱な実装があれば、どれだけ呼び出しを集約しても意味がありません。Wrapper は信頼境界なので、レビューとテストを手厚くしてください。
  • サードパーティSDKの内部動作: 自分たちのソースコードしか対象にできません。SDKが勝手にログ出力や通信をする場合は、別の対策(SDKの選定、通信の監視)が必要です。
  • ビルド成果物の検査: ソースを解析するだけで、実際に出力されたログや APK の中身は検査しません。リリースビルドで android.util.Log の呼び出しを R8 で削除するといった、成果物側の対策と組み合わせると安心です。

まとめ

  • 共通Wrapperを用意しても、ルールを文書やレビューだけに頼ると直接呼び出しは防げない。ビルドで機械的に強制する
  • サードパーティライブラリは、implementation によるモジュール分離で本物のコンパイルエラーにできる
  • Android標準APIは、detekt の ForbiddenImport(import)と ForbiddenMethodCall(完全修飾名の直接呼び出し)で検知する。ワイルドカードimportは WildcardImport で別途禁止する
  • ForbiddenMethodCall は型解決が必須。check に自動で含まれるのは型解決なしの detekt だけなので、detektDebug などを明示的に組み込まないと素通りする
  • @Suppress や型参照だけの使い方は detekt をすり抜けるため、grep ベースのゲートと CODEOWNERS で最後の穴を塞ぐ
  • ルールだけでなく、Wrapper の品質と、成果物側の対策(R8によるログ削除など)も併せて運用する

「書かないでください」というルールを、「書いたらビルドが通らない」仕組みに変える。それが、うっかりを減らす現実的な一歩です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?