İçeriğe geç
academia.sh

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

  • $_FILES her dosya alanı için name, size, error ve tmp_name taşır; move_uploaded_file yalnız bu isteğin aldığı dosyayı taşır.
  • Bu dersin ölçü sınıfı dönüştür: yüklenmemiş dosyada move_uploaded_file uyarı vermeden false döndürür.
  • upload_max_filesize aşılınca alan $_FILES içinde kalır, size 0 ve error UPLOAD_ERR_INI_SIZE olur; istek yine 200 verir.
  • Dosya alanı gönderilmezse $_FILES boş, $_POST dolu kalır.
  • post_max_size aşılınca $_POST ve $_FILES ikisi 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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat