Bu örnekte önemli bir hata ile karşılaşıyoruz: e-posta içindeki neredeyse her link sorunsuz çalışırken, özellikle kritik olanlar — imzalı linkler — sessizce bozuluyor. Bu durum, Laravel’in istekleri reddetmesine neden oluyor. Bugün mail-history için tek satırlık bir düzeltme gönderdim ve bunun nedenini anlamak, kod değiştirmekten daha ilginç.
<h2>Kurulum: Kendin Yönettiğin Tıklama Takibi</h2>
<p>mail-history, kendin yönettiğin bir açılma/tıklama takibi yapıyor. Üçüncü taraf bir piksel servisi kullanmadan, çıkan e-postanın HTML'sini yeniden yazarak her <code><a href=""></code> bağlantısını önce bir yönlendirme uç noktasına yönlendiriyor. Bu yönlendirme, tıklamayı kaydettikten sonra kullanıcıyı gerçek hedefe yönlendiriyor.</p>
<p>Yolculuk sırasında, orijinal URL'yi takip linkine şifreliyoruz ve çıkışta çözüyoruz. Yeniden yazma işlemi, hem Mailable kaygılarını hem de enjekte edilen dinleyiciyi paylaşan bir trait içinde yer alıyor; böylece mantık tam olarak bir yerde mevcut:</p>
<div class="highlight js-code-highlight">
<pre class="highlight php"><code><span class="k">protected</span> <span class="k">function</span> <span class="n">rewriteClickLinks</span><span class="p">(</span><span class="kt">string</span> <span class="nv">$html</span><span class="p">,</span> <span class="kt">string</span> <span class="nv">$hash</span><span class="p">):</span> <span class="kt">string</span>{
if ($hash === ” || $html === ”) {
return $html;
}
<span class="nv">$excludePatterns</span> <span class="o">=</span> <span class="p">(</span><span class="k">array</span><span class="p">)</span> <span class="nf">config</span><span class="p">(</span><span class="s1">'mailhistory.tracking.click.exclude_patterns'</span><span class="p">,</span> <span class="p">[</span><span class="s1">'*unsubscribe*'</span><span class="p">]);</span>
<span class="k">return</span> <span class="p">(</span><span class="n">string</span><span class="p">)</span> <span class="nb">preg_replace_callback</span><span class="p">(</span>
<span class="s1">'/<a>]*?)href=["\']([^"\']+)["\']/i'</span><span class="p">,</span>
<span class="k">function</span> <span class="p">(</span><span class="nv">$matches</span><span class="p">)</span> <span class="k">use</span> <span class="p">(</span><span class="nv">$hash</span><span class="p">,</span> <span class="nv">$excludePatterns</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$attributes</span> <span class="o">=</span> <span class="nv">$matches</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>
<span class="nv">$originalUrl</span> <span class="o">=</span> <span class="nv">$matches</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span>
<span class="c1">// mailto:/tel:/#/javascript: ve hariç olan desenleri atla...</span>
<span class="nv">$trackingUrl</span> <span class="o">=</span> <span class="nf">route</span><span class="p">(</span><span class="s1">'mailhistory.tracking.click'</span><span class="p">,</span> <span class="p">[</span>
<span class="s1">'hash'</span> <span class="o">=></span> <span class="nv">$hash</span><span class="p">,</span>
<span class="s1">'url'</span> <span class="o">=></span> <span class="nc">Crypt</span><span class="o">::</span><span class="nf">encryptString</span><span class="p">(</span><span class="nv">$originalUrl</span><span class="p">),</span>
<span class="p">]);</span>
<span class="k">return</span> <span class="s1">'<a><span class="mf">.</span><span class="nv">$attributes</span><span class="mf">.</span><span class="s1">'href="'</span><span class="mf">.</span><span class="nb">htmlspecialchars</span><span class="p">(</span><span class="nv">$trackingUrl</span><span class="p">)</span><span class="mf">.</span><span class="s1">'"'</span><span class="p">;</span>
<span class="p">},</span>
<span class="nv">$html</span>
<span class="p">);</span>}
<p>Şu satırı dikkatlice okuyun: <code>$originalUrl = $matches[2]</code>. URL'yi, <em>gerçekleşmiş</em> HTML'den doğrudan alıyoruz.</p>
<h2>Tuzağı: Render Edilmiş HTML Kaçırılmış HTML'dir</h2>
<p>Trait, e-posta içeriğini gördüğünde, Blade ve Laravel'in mail şablonları işlerini yapmış olur — ve bu işlerden biri, özellikleri HTML'de kaçırmaktır. Bir <code>href</code>'de ampersand, ampersand olarak kalmaz. <code>&</code> haline gelir.</p>
<p>Normal bir bağlantı için bu görünmezdir çünkü tarayıcı bunu geri çözer. Ancak, biz bunu bir tarayıcıya vermiyoruz. <code>Crypt::encryptString()</code>'i ham yakalanmış dize üzerinde çağırıyoruz. Yani Laravel imzalı bir URL'yi render ettiğinde:</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>https://example.com/email/verify/1/abc?expires=123&signature=deadbeef
</code></pre>
</div>
<p><code>href</code> niteliğinde aslında şunlar var:</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>https://example.com/email/verify/1/abc?expires=123&signature=deadbeef
</code></pre>
</div>
<p>Biz bunu <em>şifreliyoruz</em> — <code>&</code> ve hepsini. Tıklandığında, yeniden yönlendirme bunu çözüyor ve kullanıcıyı <code>?expires=123&signature=deadbeef</code> olan bir URL'ye yönlendiriyor. Laravel'in imzalı URL doğrulaması Query String üzerindeki imzayı tekrar hesaplıyor; şimdi <code>amp;</code> içeren bir değer görüyor ki, bu daha önce imzalanan herhangi bir şeyin parçası değildi. Doğrulama başarısız olur.</p>
<p>Acı olan taraf ise hata durumu. Normal pazarlama bağlantıları — <code>https://example.com/page</code> — Query String'e sahip değil; dolayısıyla, geçerler ve sağlıklı görünürler. Hızlı bir testte her şeyin doğru göründüğünü sanabilirsiniz. Ancak, yalnızca imzalı URL'ler, <code>expires</code> ve <code>signature</code> <code>&</code> ile ayrılmış olanlar, sessizce ölüyor. Bu hata, güvenlikle ilgili en hassas bağlantılarınızı hedef alıyor ve geri kalanlarını yalnızca bırakıyor.</p>
<h2>Düzeltme: Okuma Öncesi Çöz</h2>
<p>HTML varlıklarını, URL'yi okuduğunuz an çözüp düzeltmeden önce tıklama durumu veya başka bir şey tarafından kullanılmasına izin vermemek önemlidir:</p>
<div class="highlight js-code-highlight">
<pre class="highlight php"><code><span class="c1">// Render edilmiş HTML'den alınan href'den (örn; & -> &) kaçarak</span>// şifreli bağlantının bütünlüğünü koru…
$originalUrl = html_entity_decode($matches[2], ENT_QUOTES | ENT_HTML5);


