Ders 13 / 14
Dosya Yükleme
Dosya yüklemede sinyalin dönüş değeri ve hata koduyla gelmesi: yüklenmemiş dosyada move_uploaded_file, sunucu sınırının aşılması ve post_max_size aşımında boşalan istek.
Yayın Güncelleme
Bu derste
- Yüklenmemiş bir dosyanın taşınmaya çalışılınca hangi kanaldan duyulduğunu ölçer
- Yerleşik sunucuya curl ile dosya göndererek $_FILES yapısını ve hata kodlarını gözlemler
- post_max_size aşımında hata koduyla dönen alanın bile kalmadığını gösterir
İçindekiler
Form İşleme dersinde geçersiz bir değer filter_var fonksiyonundan false olarak döndü ve istek 200 ile sürdü. Dosya yükleme aynı kanalı kullanır, ama istekte iki parça vardır: dosyanın kendisi ve PHP’nin ona eşlik ettirdiği durum bilgisi. Soru da genişler: yükleme başarısız olduğunda bu bilgi nerede, ve hiç gelmezse ne olur?
Yüklemenin HTTP tarafını, yani çok parçalı gövdenin yapısını M04, bir isteğin sunucuda nasıl ele alındığını M16/K01 kurdu. Burada ölçülen, PHP’nin bu istekten ne çıkardığıdır. Yüklenen dosya $_FILES süper küresel değişkenine konur; her dosya alanı için name (istemcinin verdiği ad), size (bayt), error (durum kodu) ve geçici dosyanın yolunu veren tmp_name anahtarları bulunur. Geçici dosya yolu ortama bağlı olduğu için bu derste hiçbir çıktıda basılmaz.
Yüklenmemiş Dosya Dönüşle Duyulur
Dosyayı kalıcı bir yere taşımanın tek yolu move_uploaded_file fonksiyonudur ve yalnız PHP’nin o istekte kendisinin aldığı bir dosya için çalışır. İlk ölçüm komut satırında yapılır; orada hiçbir yükleme olmamıştır, bu yüzden fonksiyon var olan sıradan bir dosyaya uygulanınca da başarısız olmalıdır.
<?php // kanal_yukleme.php — yuklenmemis bir dosyanin tasinmasi hangi kanaldan duyulur $uyarilar = []; set_error_handler(function (int $seviye) use (&$uyarilar): bool { $uyarilar[] = $seviye === E_WARNING ? 'E_WARNING' : 'E_' . $seviye; return true; }); file_put_contents('kaynak.txt', "merhaba\n"); try { $sonuc = move_uploaded_file('kaynak.txt', 'hedef.txt'); $sinif = $uyarilar ? 'uyari' : ($sonuc === false ? 'donus' : 'sessiz'); } catch (Throwable $hata) { $sinif = 'durdurur'; } printf("%-24s%-10s%s\n", 'yuklenmemis dosya', $sinif, 'sonuc ' . var_export($sonuc, true)); printf("%-24s%s\n", 'hedef olustu mu', file_exists('hedef.txt') ? 'evet' : 'hayir');
yuklenmemis dosya donus sonuc false hedef olustu mu hayir
Fonksiyon false döndürür; uyarı yok, istisna yok. Dosya hedefe taşınmadı. Bu dönüş sınıfıdır ve bu derste ölçülen kanal budur: yürütme, çağıran false değerini sorgulamazsa hiçbir şey olmamış gibi sürer. IY15. Ölçüm, fonksiyonun yalnız PHP’nin bir isteğin parçası olarak aldığı dosyalarda çalışmasının bilinçli bir güvenlik özelliği olduğunu gösterir: çağıran bir dosya adını elle verse bile fonksiyon onu taşımaz. Bu özelliğin neden var olduğu ve sınıflandırması M24 ile M25’in konusudur.
Yüklenen Dosyanın Yapısı
Gerçek bir yüklemeyi yerleşik sunucu ve curl -F ile ölçeriz. -F bayrağı isteği çok parçalı gövdeyle gönderir ve @ öneki alanın değerini bir dosyadan okutur.
<?php // yukle.php — yuklenen dosyayi ve alan sayilarini raporlar $adlar = [ UPLOAD_ERR_OK => 'UPLOAD_ERR_OK', UPLOAD_ERR_INI_SIZE => 'UPLOAD_ERR_INI_SIZE', UPLOAD_ERR_NO_FILE => 'UPLOAD_ERR_NO_FILE', ]; echo 'files=', count($_FILES), ' post=', count($_POST), "\n"; foreach ($_FILES as $alan => $dosya) { echo $alan, ' name=', $dosya['name'], ' size=', $dosya['size']; echo ' error=', $adlar[$dosya['error']] ?? $dosya['error'], "\n"; if ($dosya['error'] === UPLOAD_ERR_OK) { $tasindi = move_uploaded_file($dosya['tmp_name'], 'saklanan.txt'); echo 'tasindi=', $tasindi ? 'true' : 'false', ' boyut=', filesize('saklanan.txt'), "\n"; } }
# sunucu.sh — kucuk bir dosyayi yerlesik sunucuya yukler printf 'merhaba dunya\n' > rapor.txt php -S 127.0.0.1:8083 -d display_errors=0 >/dev/null 2>&1 & pid=$! for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8083/yukle.php && break; sleep 0.1; done curl -s -F '[email protected]' -F 'baslik=ilk' -w 'durum=%{http_code}\n' http://127.0.0.1:8083/yukle.php kill $pid
files=1 post=1 dosya name=rapor.txt size=14 error=UPLOAD_ERR_OK tasindi=true boyut=14 durum=200
$_FILES bir dosya alanı gördü, $_POST ise metin alanını (baslik). Alan adı, istemcinin verdiği dosya adı, bayt cinsinden boyut ve UPLOAD_ERR_OK hata kodu geldi; move_uploaded_file bu kez true döndü, çünkü dosya bu isteğin parçasıydı. Dönüş değerinin true olduğu tek durum budur: fonksiyon, yürütmenin başında doğru yoldan gelmiş bir dosyayı kabul eder. IY16. tmp_name bu çıktıda yoktur; değer her koşumda değişir ve ders onu basmaz.
$_FILES Yapısı ve Dosya Seçilmemiş Alan
Şimdiye kadar yalnız üç anahtar okundu. Bir dosya alanının tüm yapısı, anahtar adlarıyla birlikte tek bir betikle görülebilir; tmp_name değeri basılmaz, yalnız anahtar adı listelenir. Aynı betik, formda dosya alanı bulunduğu hâlde kullanıcının dosya seçmediği durumu da ölçer: bu durumda tarayıcı alanı boş bir dosya adıyla gönderir.
<?php // yapi.php — $_FILES alanlarinin tum anahtarlari ve hata kodlari foreach ($_FILES as $alan => $dosya) { $anahtarlar = array_keys($dosya); sort($anahtarlar); echo $alan, ' anahtarlar=', implode(',', $anahtarlar), "\n"; echo $alan, ' error=', json_encode($dosya['error']), ' size=', json_encode($dosya['size']), "\n"; } echo 'dosya_var=', isset($_FILES['dosya']) ? 'evet' : 'hayir', "\n";
# yapi.sh — dolu alan ve dosya secilmemis alan printf 'merhaba\n' > rapor.txt php -S 127.0.0.1:8083 -d display_errors=0 >/dev/null 2>&1 & pid=$! for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8083/yapi.php && break; sleep 0.1; done echo '--- dolu alan' curl -s -F '[email protected]' http://127.0.0.1:8083/yapi.php echo '--- dosya secilmemis' curl -s -F 'dosya=@/dev/null;filename=' http://127.0.0.1:8083/yapi.php kill $pid
--- dolu alan dosya anahtarlar=error,full_path,name,size,tmp_name,type dosya error=0 size=8 dosya_var=evet --- dosya secilmemis dosya anahtarlar=error,full_path,name,size,tmp_name,type dosya error=4 size=0 dosya_var=evet
Dolu alan error 0 ve size 8 verir; anahtar listesi ölçülen PHP 8.5 sürümünde full_path, name, size, tmp_name, type ve error anahtarlarını içerir. IY17. full_path anahtarı 8.1 sürümünden beri vardır; ders anahtar listesini yalnız ölçülen sürüm için gösterir. Dosya seçilmemiş alanda ise error UPLOAD_ERR_NO_FILE değeri olan 4 gelir ve size 0’dır. Bu, $_FILES['dosya'] anahtarının var olduğu ama yüklemenin yapılmadığı durumdur: isset bunu evet diye bildirir, yani anahtarın varlığı yüklemenin başarısını göstermez. Başarı, error değerinin UPLOAD_ERR_OK olmasıyla anlaşılır.
Çoklu Dosya Dizisi Alan Başına Değil Anahtar Başınadır
Alan adı dosya[] ile verilince PHP her dosya için ayrı bir kayıt oluşturmaz. Bunun yerine her anahtarın (name, size, error) altında dosyaların dizisini tutar; yani yapı dosya başına bir kayıt gibi değil, anahtar başına bir dizi gibi görünür.
<?php // coklu.php — dosya[] alaninin $_FILES'taki yapisi $dosya = $_FILES['dosya']; echo 'name_tipi=', get_debug_type($dosya['name']), ' adet=', count($dosya['name']), "\n"; foreach ($dosya['name'] as $sira => $ad) { echo $sira, ' ', $ad, ' size=', $dosya['size'][$sira], ' error=', $dosya['error'][$sira], "\n"; }
# coklu.sh — iki dosyayi tek alan adiyla yukler printf 'bir\n' > a.txt printf 'iki iki\n' > b.txt php -S 127.0.0.1:8083 -d display_errors=0 >/dev/null 2>&1 & pid=$! for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8083/coklu.php && break; sleep 0.1; done curl -s -F 'dosya[][email protected]' -F 'dosya[][email protected]' -w 'durum=%{http_code}\n' http://127.0.0.1:8083/coklu.php kill $pid
name_tipi=array adet=2 0 a.txt size=4 error=0 1 b.txt size=8 error=0 durum=200
name bir array olur ve her dosyanın bilgisi aynı sıra numarasıyla üç ayrı diziden okunur. Tek dosya için yazılan $dosya['name'] okuması, çoklu yüklemede bir diziyi dizgi gibi kullanmaya çalışır; bu, Süper Küresel Değişkenler dersindeki strlen ve dizi ölçümünün aynı hatasıdır. IY18. İstemci alan adını dosya[] yaparak yapıyı değiştirebildiği için, tek dosya bekleyen kod için yapı yine istemcinin seçimidir.
Hedef Yol Yoksa Taşıma Uyarı Verir
Taşıma fonksiyonu iki ayrı nedenle false döndürebilir: dosya bu isteğin parçası değildir ya da hedef yazılamaz. İlki sessizdir. İkincisi aynı dönüş değerini bir uyarıyla birlikte verir. Aşağıdaki betik gerçek bir yükleme alır ve dosyayı olmayan bir dizine taşımaya çalışır; uyarının metni basılmaz, yalnız seviye adı basılır.
<?php // hedef.php — yuklenen dosyayi olmayan bir dizine tasima denemesi $seviyeler = []; set_error_handler(function (int $seviye) use (&$seviyeler): bool { $seviyeler[] = $seviye === E_WARNING ? 'E_WARNING' : 'E_' . $seviye; return true; }); $sonuc = move_uploaded_file($_FILES['dosya']['tmp_name'], 'olmayan_dizin/hedef.txt'); echo 'sonuc=', var_export($sonuc, true), ' uyari=', implode(',', $seviyeler), "\n";
# hedef.sh — dosyayi gonderir, tasima olmayan dizine yapilir printf 'merhaba\n' > rapor.txt php -S 127.0.0.1:8083 -d display_errors=0 >/dev/null 2>&1 & pid=$! for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8083/hedef.php && break; sleep 0.1; done curl -s -F '[email protected]' -w 'durum=%{http_code}\n' http://127.0.0.1:8083/hedef.php kill $pid
sonuc=false uyari=E_WARNING,E_WARNING durum=200
Fonksiyon false döndürür ve ek olarak iki E_WARNING bildirir: biri hedefin açılamamasını, öteki taşımanın başarısızlığını söyler. Aynı dönüş değeri, ilk bölümdeki yüklenmemiş dosyada uyarısız gelmişti; burada sınıf, uyarıya kayar. IY19. Bu fark dönüş değerine bakan kod için görünmezdir: ikisi de false‘tur. Yalnız hata işleyicisi ya da kayıt dosyası olan kod, taşımanın neden başarısız olduğunu ayırt edebilir; durum kodu yine 200’dür.
Sunucu Sınırı Aşılırsa
PHP iki ini değeriyle yüklemeyi sınırlar: upload_max_filesize tek bir dosyanın, post_max_size ise tüm isteğin üst sınırıdır. Ders hiçbir ayarın varsayılanına güvenmez; sınır -d ile verilir. İlk ölçümde tek dosya sınırı küçük tutulur ve büyük bir dosya gönderilir. İkinci ölçümde dosya alanı hiç gönderilmez.
# sinir.sh — dosya sinirinin asilmasi ve dosya alaninin gonderilmemesi head -c 3000 /dev/zero | tr '\0' 'a' > buyuk.txt php -S 127.0.0.1:8083 -d display_errors=0 -d upload_max_filesize=1K >/dev/null 2>&1 & pid=$! for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8083/yukle.php && break; sleep 0.1; done echo '--- 3000 bayt, sinir 1K' curl -s -F '[email protected]' -F 'baslik=ikinci' -w 'durum=%{http_code}\n' http://127.0.0.1:8083/yukle.php echo '--- dosya alani yok' curl -s -F 'baslik=ucuncu' -w 'durum=%{http_code}\n' http://127.0.0.1:8083/yukle.php kill $pid
--- 3000 bayt, sinir 1K files=1 post=1 dosya name=buyuk.txt size=0 error=UPLOAD_ERR_INI_SIZE durum=200 --- dosya alani yok files=0 post=1 durum=200
Sınır aşıldığında alan $_FILES içinde yine vardır: adı korunur, size 0 olur ve error UPLOAD_ERR_INI_SIZE olur. Sinyal bu kez hata kodunda gelir; kod okunmazsa dosya hiç gelmemiş gibi sürülür. İstek yine 200 verir. Dosya alanı hiç gönderilmediğinde ise $_FILES boştur ve $_POST doludur: kodun $_FILES['dosya']’yı doğrudan okuması hem uyarı hem null demektir, yani Süper Küresel Değişkenler dersindeki eksik anahtardır.
Dosyayı saklarken istemcinin verdiği adı yola doğrudan koymamak, türü yalnız uzantıdan çıkarmamak ve kalıcı yeri belge köküne koymamak gerekir; bu ilkelerin gerekçesi M16/K01’de, sınıflandırması ve savunma yordamları M24 ile M25’te anlatılır. Bu derste saklanan.txt sabit bir ad olarak yalnız ölçümü tekrarlanabilir kılmak için kullanıldı.
Sınırlayıcı Ölçüm
Önceki bölümde hata koduyla dönen alan vardı. Sınırı daha da aşağı çekersek bu da kalmaz. post_max_size aşılınca PHP isteğin gövdesini hiç ayrıştırmaz: ne dosya ne metin alanı elde kalır.
# tavan.sh — post_max_size asildiginda ne kaliyor head -c 5000 /dev/zero | tr '\0' 'a' > buyuk.txt php -S 127.0.0.1:8083 -d display_errors=0 -d post_max_size=1K >/dev/null 2>&1 & pid=$! for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8083/yukle.php && break; sleep 0.1; done curl -s -F '[email protected]' -F 'baslik=dorduncu' -w 'durum=%{http_code}\n' http://127.0.0.1:8083/yukle.php kill $pid
files=0 post=0 durum=200
$_FILES da $_POST da boştur ve durum kodu 200’dür. Hata koduyla gelen bir alan bile yok: istek, hiç alan göndermemiş bir istekten ayırt edilemez. IY20. display_errors=0 ile ölçüldü; ayar açıkken PHP’nin başlangıçta ürettiği uyarı gövdeye düşebilir ve ders o metni basmaz. Bu, önceki bölümdeki sınıfın alt sınırıdır: dönüş kanalı, kanalın konuşacağı değerin kendisi geldiğinde işe yarar. Değer hiç gelmediğinde ise okunacak hata kodu da kalmaz. Bu durumu kodun fark etmesinin tek yolu, beklediği alanların yokluğunu ayrı bir koşul olarak sınamasıdır.
Özet
$_FILESher dosya alanı içinname,size,errorvetmp_nametaşır;move_uploaded_fileyalnız bu isteğin aldığı dosyayı taşır.- Bu dersin ölçü sınıfı dönüştür: yüklenmemiş dosyada
move_uploaded_fileuyarı vermedenfalsedöndürür. upload_max_filesizeaşılınca alan$_FILESiçinde kalır,size0 veerrorUPLOAD_ERR_INI_SIZEolur; istek yine 200 verir.- Dosya alanı gönderilmezse
$_FILESboş,$_POSTdolu kalır. post_max_sizeaşılınca$_POSTve$_FILESikisi de boşalır; hata kodu taşıyan alan bile kalmaz ve istek sinyalsiz boş bir istek gibi görünür.
Sonraki Adım
Yükleme dersinde kod, yukle.php gibi bir betiğe kendi yazdığı dosyayı verip bir yanıt aldı. Bir betiğin başka bir dosyayı kendi içine çekmesi ise ayrı bir işlemdir ve aynı sorunun yeni bir biçimini getirir: çekilecek dosya bulunamazsa ne olur? Dosya Dahil Etme dersi, bulunamayan bir dosyanın dahil edilmesinin hangi kanaldan duyulduğunu ve bu kanalın dahil etme türüne göre nasıl değiştiğini ölçüyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.