# Tema Formlarını Güvenli Hale Getirme

https://dev.wisecp.com/tr/tema-formlarini-guvenli-hale-getirme

Bir public form çerçeve tarafından korunmaz. Korumaları şablon gösterir, operation uygular.

## Genel Bakış

Tema formunu koruyan bir ara katman yoktur. Çifti siz yazarsınız: view jeton ve doğrulama kutusu gösterir, operation onları sabit sırada denetler. İki yarımı form anahtarı birleştirir.

Aynı istek üzerinde beş katman durur. Dördü *kimin gönderdiğini* yargılar, beşincisi *ne gönderildiğini*.

## Ön Koşullar

- Smarty ya da Twig kullanan bir tema; `{csrf}` ve `{captcha}` ikisinde de vardır.
- Gönderimi alacak bir operation ([Operation'lar](https://dev.wisecp.com/tr/operationlar)).
- Eşikler operatörün, `coremio/configuration/options.php` içinde. Bir limiti asla sabit yazmayın.

## Yapı

### Beş Katman

| Katman | Neyi durdurur | Eksik bırakmak neyi açar |
| --- | --- | --- |
| **CSRF** | Sizin formunuzdan gelmeyen gönderimleri. | Herhangi bir sayfa, giriş yapmış bir oturumla adresinize gönderim yapabilir. |
| **İşlem kısıtlama** | Tek adresten gelen tekrarı; limit aşılınca süreli sert blok. | Tek bir adres uç noktayı süresiz meşgul edebilir. |
| **Bot kalkanı** | Otomasyonu; doğrulamayı eşik aşıldıktan sonra ister. | Otomatik bir istemci doğrulamayla hiç karşılaşmaz; form bir kâhine dönüşür. |
| **Captcha (statik)** | Operatörün kilitlediği bir alandaki her gönderimi. | Kapatılan alan açık kalır ve panelde ayar açık görünür. |
| **Spam kapısı** | Yasak kelimeleri, geçici posta kutularını, itibar listelerini. | Hiçbir içerik kuralı çalışmaz; boş kalan liste korumayı sağlam gösterir. |

Her katman tek bir çağrıdır ve kanıtı o çağrıdır.

- **Validation::verify_csrf_token()**: Jeton katmanı. False sonucu isteği anında bitirmelidir.
- **ProcessRestriction::blocked()**: Sert blok katmanı. Yalnız sorar; isteği `hit()` sayar.
- **BotShield::triggered()**: Uyarlanan katman. True, bu adresin doğrulama yanıtlaması demektir.
- **Captcha::enabled()**: Statik katman. Alan bazlı anahtarı okur; bir öncekiyle OR ile birleşir.
- **Validation::spam_guard()**: İçerik katmanı. Engelleme gerekçesini döner ve denemeyi kaydeder.

> **Sıra korumanın bir parçasıdır**
> 
> Önce jeton, sonra sert blok, doğrulama, içerik, en sonda iş. Girdiyi erken okumak saldırganı ayrıştırıcınıza ulaştırır.

### Hangi Yarım Hangi Parçanın Sahibi

| Parça | Tema tarafı | Sunucu tarafı |
| --- | --- | --- |
| Jeton | `{csrf form='contact-form'}` gizli bir `token` input'u koyar. | Aynı anahtarla doğrulanır. |
| Doğrulama | `{captcha area='contact-form'}` sağlayıcının kutusunu koyar. | Captcha yardımcısı istekten okur. |
| İstek başlığı | fetch çağrısı `X-Requested-With` gönderir. | Üçüncü argüman aksini söylemedikçe şarttır. |
| Eşikler | Hiçbir şey; tema limit okumaz. | Yardımcılar operatörün yapılandırmasından okur. |

## Adım Adım

### 1. Formu İşaretleyin

1. Tek bir anahtar seçip her yerde kullanın: jeton, captcha alanı, kısıtlama eylemi.
2. Jetonu formun içine koyun; gönderilen alanların parçası olmalıdır.
3. Captcha'yı butonun yanına koyun; o alan kapalıyken hiçbir şey görünmez.

```smarty
<form action="{link route='contact'}" method="post" novalidate data-contact-form>

    <input type="text"  class="form-control" name="name"  required>
    <input type="email" class="form-control" name="email" required>
    <textarea class="form-control" name="message" rows="6" required></textarea>

    {* Handler'ın doğrulama anahtarıyla aynı dize. <input type="hidden" name="token"> basar. *}
    {csrf form='contact-form'}

    <div class="d-flex flex-wrap align-items-center gap-3 border-top pt-3 mt-4">
        {* Operatör bu alanda captcha'yı kapattıysa '' basar, satır yine de düzgün dizilir. *}
        {captcha area='contact-form'}
        <button class="btn btn-primary ms-sm-auto" type="submit">{lang key='website/contact/send-button'}</button>
    </div>
</form>
```

### 2. Handler'ı Koruyun

1. İsteğe dokunmadan önce jetonu doğrulayın.
2. Bu adres sert bloklu mu diye sorun ve blokluysa durun.
3. Doğrulama gerekli mi karar verin: alan kilitliyse *ya da* kalkan devredeyse gereklidir.
4. Girdileri okuyup doğrulayın, sonra spam kapısını çalıştırın.
5. İşi yapın, pencereyi kapatın: kısıtlama ve kalkan sayaçlarını güncelleyin.

```php
// 1. Jeton. İstekten hiçbir şey okunmadan önce.
if (!\Validation::verify_csrf_token((string) Filter::init("POST/token", "hclear"), "contact-form"))
    throw new \Exception(Language::g("needs/csrf-failed"));

// 2. Sert blok. Bu adres limiti çoktan aştı ve süresini dolduruyor.
if (\ProcessRestriction::blocked("contact-form"))
    throw new \Exception(Language::gc("website/contact/rate-limited"));

// 3. Doğrulama. Operatörün kilitlediği alan VEYA kalkanı tetiklemiş adres.
$needCaptcha = \Captcha::enabled("contact-form") || \BotShield::triggered("contact-form");
if ($needCaptcha && !(new \Captcha())->check()) {
    \BotShield::record("contact-form");
    return $operation->output([
        "status"  => "captcha_required",
        "message" => Language::gc("website/contact/captcha-required"),
    ]);
}

// 4. İçerik ve gönderen; doğrulamadan sonra, yazmadan önce.
if (\Validation::spam_guard($full_name, $message, $email, $phone, $ip) !== '')
    throw new \Exception(Language::g("needs/spam-blocked"));

// 5. ... asıl iş ...

// 6. Pencereyi kapat.
\ProcessRestriction::hit("contact-form");
if ($needCaptcha) \BotShield::clear("contact-form");
else              \BotShield::record("contact-form");
```

### 3. İsteği Gönderin

1. Gövdeyi formdan kurun; `FormData` jetonu ve yanıtı taşır.
2. AJAX başlığını gönderin, yoksa jeton doğrulaması reddeder.
3. Üçüncü sonucu ele alın: `captcha_required` bir ret değil sorudur.
4. Doğrulamayı finally dalında tazeleyin; yanıt tek kullanımlıktır.

```javascript
var fd = new FormData(form);          // jeton + captcha yanıtı formun içindeki adlandırılmış input'lardır

fetch(endpoint, {
    method: 'POST',
    body: fd,
    headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
    .then(function (r) { return r.json(); })
    .then(function (res) {
        if (res.status === 'captcha_required') {
            captchaRequired = true;               // aklında tut; sonraki boş gönderim yerelde durdurulur
            captchaMsg      = res.message || captchaMsg;
            revealCaptcha();
            setAlert(captchaMsg, 'warning');
            return;
        }
        if (res.status !== 'successful') { setAlert(res.message, 'danger'); return; }
        captchaRequired = false;
        showDoneStep(res);
    })
    .finally(function () {
        if (typeof window.wcpCaptchaRefresh === 'function') window.wcpCaptchaRefresh();
    });
```

## Referans

### Şablon Fonksiyonları

| Çağrı | Ne basar | Ne zaman hiçbir şey basmaz |
| --- | --- | --- |
| `{csrf form='<anahtar>'}` | Gizli bir `token` input'u: anahtarın oturum sırrıyla HMAC'i. | Hiçbir zaman. Boş anahtar da bir anahtardır; onu kullanan formlar tek jeton paylaşır. |
| `{captcha area='<alan>' tray='<id>'}` | Sağlayıcının kutusu tema yuvasında; `tray` verildiyse tepsi içinde. | Captcha kapalıyken *ve* kalkan devrede değilken. Satırı onsuz da doğru tasarlayın. |

İkisi Twig için de kaydedilir; orada seçeneklerin *sırası* vardır: `captcha(area, tray, class, force)` ve `csrf(form)`.

### Yardımcı İmzaları

Argüman sırasını dikkatle okuyun: jeton fonksiyonları anahtarı *ikinci* sırada alır.

```php
// coremio/classes/Validation.php

// $input = true hazır <input type="hidden" name="token"> döner; false çıplak jetonu döner.
public static function get_csrf_token($form_index = '', $input = true);

// ANAHTAR İKİNCİ ARGÜMANDIR. $nonAjax = true, X-Requested-With şartını kaldırır.
public static function verify_csrf_token($incoming_data = '', $form_index = '', $nonAjax = false);

// '' temiz demektir. Boş olmayan dize, engellenenler listesine zaten yazılmış, operatöre dönük gerekçedir.
public static function spam_guard(string $subject = '', string $message = '', string $email = '', string $phone = '', string $ip = '', string $domain = ''): string;
```

```php
// coremio/helpers/processrestriction.php
// $ip = null çağıranın adresini kendisi çözer, proxy ve CDN farkındadır. Yalnız mevcut
// ziyaretçininkinden başka bir adresi yargılarken bir değer geçin.

public static function blocked(string $action, ?string $ip = null): bool;   // şu an süresini dolduruyor mu
public static function hit(string $action, ?string $ip = null): bool;       // bir tane say; true = artık bloklu
public static function clear(string $action, ?string $ip = null): void;     // sayacı ve bloğu sıfırla
```

```php
// coremio/helpers/botshield.php

public static function active(string $action): bool;                        // bu eylem için kurulu mu
public static function triggered(string $action, ?string $ip = null): bool; // bu adres doğrulama istiyor mu
public static function record(string $action, ?string $ip = null): void;    // doğrulamasız bir deneme say
public static function clear(string $action, ?string $ip = null): void;     // bir doğrulama çözüldü
```

```php
// coremio/helpers/captcha.php

public static function enabled(string $area = ''): bool;                    // operatör bu alanı açtı mı
public static function widget(string $area = '', array $opts = []): string; // {captcha} bunu çağırır
public function check(): bool;                                              // örnek metodu: (new Captcha())->check()
```

### Widget Seçenekleri

- **tray**: Kutuyu taşıyacak tepsinin id'si. Harf, rakam, alt çizgi ve tireye indirgenir.
- **class**: Yuva sarmalayıcısının yardımcı sınıfları.
- **force**: Alan anahtarını ve kalkanı yok sayarak her zaman görünür. Bu bir karardır, varsayılan değil.

### Üç Yanıt Biçimi

- **successful**: İş gerçekleşti; formu bir onay adımıyla değiştirebilirsiniz.
- **captcha_required**: Sunucu reddetmiyor, soruyor. Kutuyu açın ve yazılan değerleri koruyun.
- **error**: JSON'a çevrilmiş bir exception; spam gerekçesi içinde değildir.

## Örnek

İletişim formu, iki yarımıyla. `contact-form` anahtarı iki tarafta da geçer; tek arama çifti kanıtlar.

```php
public function submit(Operation $operation): bool
{
    $operation->demo();

    if (!\Validation::verify_csrf_token((string) Filter::init("POST/token", "hclear"), "contact-form"))
        throw new \Exception(Language::g("needs/csrf-failed"));

    if (\ProcessRestriction::blocked("contact-form"))
        throw new \Exception(Language::gc("website/contact/rate-limited"));

    $needCaptcha = \Captcha::enabled("contact-form") || \BotShield::triggered("contact-form");
    if ($needCaptcha && !(new \Captcha())->check()) {
        \BotShield::record("contact-form");
        return $operation->output([
            "status"  => "captcha_required",
            "message" => Language::gc("website/contact/captcha-required"),
        ]);
    }

    $full_name = trim((string) Filter::init("POST/name", "hclear"));
    $email     = trim((string) Filter::init("POST/email", "email"));
    $phone     = trim((string) Filter::init("POST/phone", "numbers"));
    $message   = trim((string) Filter::init("POST/message", "hclear"));
    $ip        = \UserManager::GetIP();

    if (\Validation::isEmpty($full_name))
        throw new \Exception(Language::gc("website/contact/error-name"));
    if (\Validation::isEmpty($email) || !\Validation::isEmail($email))
        throw new \Exception(Language::gc("website/contact/error-email"));
    if (\Validation::isEmpty($message) || mb_strlen($message) < 5)
        throw new \Exception(Language::gc("website/contact/error-message"));

    // İçerik kuralları saklanmak üzere olan değerlerde koşar, ham istekte değil.
    if (\Validation::spam_guard($full_name, $message, $email, $phone, $ip) !== '')
        throw new \Exception(Language::g("needs/spam-blocked"));

    $message_id = $this->model->add([
        'full_name' => $full_name,
        'email'     => $email,
        'phone'     => $phone,
        'message'   => $message,
        'ip'        => $ip,
        'cdate'     => \DateManager::Now(),
    ]);

    \ProcessRestriction::hit("contact-form");
    if ($needCaptcha) \BotShield::clear("contact-form");
    else              \BotShield::record("contact-form");

    return $operation->output([
        "status"  => "successful",
        "message" => Language::gc("website/contact/success"),
        "email"   => $email,
    ]);
}
```

İki genişleme noktası: yazmadan önce veto kancası, sonrasında olay kancası.

```php
// Boş olmayan bir dize döndüren her dinleyici gönderimi o mesajla reddeder.
foreach (\Hook::run('gate:client.contact_submit', $full_name, $email, $phone, $message, $ip) as $veto)
    if (is_string($veto) && $veto !== '') throw new \Exception($veto);

// Yazmadan sonra. Dönüş değerleri yok sayılır; bu bir duyurudur, karar değil.
\Hook::run('action:client.contact_submitted', $message_id, $full_name, $email, $phone, $message, $ip);
```

- **gate:client.contact_submit**: Doğrulamadan sonra, kayıt yazılmadan önce çalışır. Boş olmayan dize bir reddir.
- **action:client.contact_submitted**: Kayıt oluştuktan sonra, ilk argümanı onun id'si olacak şekilde çalışır. Dönüş yoksayılır.

## Tuzaklar

> **Yanlış yazılmış bir anahtar her gönderimi sonsuza dek reddeder**
> 
> Jeton, anahtarın HMAC'idir. View bir anahtar, handler başkasını kullanırsa ziyaretçi süresi dolmuş oturum mesajı görür. Uyarı da kayıt da yoktur.

> **AJAX başlığı olmadan jeton denetimi tasarım gereği reddeder**
> 
> Doğrulama, üçüncü argüman kapatmadıkça `X-Requested-With` ister. Eksik başlık yanlış anahtarla aynı belirtiyi verir.

> **İstemci kapısını sunucunun yanıtına bağlayın, kutunun görünürlüğüne değil**
> 
> Bir tepsi, ziyaretçi yazmaya başladığı için de açılabilir. Yalnız `captcha_required` yanıtının kurduğu bir işaret tutun.

> **Sonuç panelini finally dalında açmayın**
> 
> Sonuç kabını her istekten sonra açmak yer tutucuyu sızdırır. Paneli yalnız başarı dalında açın.

> **Dört bot katmanı beşincisini getirmez**
> 
> Kısıtlama katmanları adresi yargılar, içeriği değil. Kusursuz jetonlu bir form bile spam kapısı çağrılana kadar yasak kelimeleri saklar.

## İlgili Makaleler

- [Giriş ve Kayıt](https://dev.wisecp.com/tr/giris-ve-kayit)
- [Tema Kancaları ve Çıktı Filtreleri](https://dev.wisecp.com/tr/tema-kancalari-ve-cikti-filtreleri)
- [Operation'lar](https://dev.wisecp.com/tr/operationlar)
- [Kullanıcı Girdisini Filtreleme](https://dev.wisecp.com/tr/kullanici-girdisini-filtreleme)
- [Captcha Modülü Yazma](https://dev.wisecp.com/tr/captcha-modulu-yazma)
