eKloe.com - Kubuntu User

Upload obrázků speciálně pro Banán

by LucaS on Dec.25, 2008, under PHP

Nedávno jsem psal PHP funkci pro uploadování obrázků na server přes HTTP. Takových funkcí je napsáno na internetu aba kuk, ale při testování na serveru (konkrétně banan.cz) jsem narazil na jednu záludnost. Klíčové ve funkci je to, že obrázky (fotky) ukládá do složek podle dne. Aby nenastala situace, že za rok bude v jedné složce 30 000 a více souborů. Po testování jsem funkci musel přepsat a veškerou komunikaci přenosu udělat pomocí FTP. Důvod nefunkčnosti je pravděpodobně ve zvláštním nastavení Apache nebo v právech pro uživatele. Na “Banánu” nelze přes HTTP a při zapnutém “safe_mode = on” vytvořit složku a nahrát do ní obrázek bez toho, aniž by složka nemusela mít všechna prává (člení i zápis) pro vlastníka, skupinu i ostatní. To je potom velmi snadný cíl útoku (testoval jsem nastavení naposledy 25.12.2008).

V dokumentaci pro safe_mode je poznámka: Note: When safe mode is enabled, PHP checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed. - Volně přeloženo, že pokud je safe_mod zapnutý, PHP si kontroluje zda soubory a adresáře mají stejné UID jako právě prováděný skript.

Když vytvoříte složku přes HTTP, tak UID bude jiné (UID odpovídajíci Apache User), něž má prováděný skript. Takže podle zapnutého safe_mode už nemáte právo na to, abyste do složky mohli v zápětí uložit obrázek přes HTTP. To se dá obejít jedině tím, že se vše provede přes protokol FTP a vše bude mít stejného vlastníka (UID i příslušné GID) jako prováděný proces. I testovaný WordPress má trochu problém tuhle konstelaci pobrat.

Jdeme do řešení:

Na konec souboru config.inc.php připíšeme následující definice:
(třída pro připojení k postgreSQL a config.inc.php jsou popsány v článku PHP třída pro připojení k postgreSQL)

// maximalni velikost uploadovane fotky 1kb = 1024b
define ('MAX_UPLOAD_FILESIZE', '2048000');
 
// lokalni cesta k fotkam
define ('DIR_PHOTO_LOCAL', '/home/www/server.cz/photky/');
 
// absolutni cesta k fotkam
define ('DIR_PHOTO', 'http://server.cz/fotky/');
 
// FTP cesta k adresari s fotkami
define ('DIR_PHOTO_FTP', 'projekt/fotky');
 
define ("FTP_SERVER", "jmeno_serveru");
define ("FTP_USER", "prihlasovaci_jmeno");
define ("FTP_PASS", "heslo");

Lokální cesty lze zjistit pomocí funkce phpinfo(). Pro jistotu uvádím příklad. Vytvořte si na serveru soubor s názvem třeba info.php a pomocí této funkce se Vám zobrazí nastavení serveru. Na řádku _SERVER["DOCUMENT_ROOT"] nebo _SERVER["SCRIPT_FILENAME"] zjistíte hlavní adresář, ve kterém se nachází web a k absolutní cestě dopíšete příslušnou cestu k fotkám.

<?php
  phpinfo();
?>

Dále si vytvoříme soubor class.functions.php a vytvoříme si do něj následující třídu. Funkce upload_photo() bude obstarávat samotné nakopírování fotky na server přes protokol FTP včetně vytvoření složky, do ktreré fotka bude uložena. Funkce si kontroluje “snad” všechny bezpečnostní předpoklady:

  • kontrola velikosti fotky
  • kontrola zda je fotka ve formátu JPG
  • zda byla fotka úspěšně přenesena z lokálního disku na server do TMP
  • zda byla správně vytvořena a nastavena složka
  • zda do ní byla uložena fotka

Pokud jsem na něco zapomně, prosím o doplnění.

 
<?php
class kloe_functions {
 
  function __construct() {
 
  }
 
 
  function upload_photo() {
 
    $db = new db();
    $db->connect();
 
    // nejprve se zkontroluje zda uploadovany obrazek  nepresahuje povolenou velikost
    if($_FILES['photo']['size'] < MAX_UPLOAD_FILESIZE) {
 
      // vytvorime cestu k adresari, kam se fotka ulozi
      define ("FILEREPOSITORY",DIR_PHOTO_LOCAL.date("Y-m-d")."/");
 
      // zjistime, zde se fotka uploadovala na server
      // pokud ano, zrovna se nachazi v adresari TMP pro docasne soubory
      if (is_uploaded_file($_FILES['photo']['tmp_name'])) {
 
        // zajistime, aby fotky byly jen ve formatu JPG
        // pro IE se kontroluje pJPG (progresive)
        if (($_FILES['photo']['type'] == "image/jpeg") ||
            ($_FILES['photo']['type'] == "image/pjpeg")) {
 
            // vytvorime nazev souboru,
            // zvolil jsem (Seconds since the Unix Epoch)
            $file_name = date("U");
 
            // nyni se pripojime k serveru pomoci FTP
            // otevreni spojeni a nasledne prihlaseni
            $ftp = ftp_connect(FTP_SERVER);
            $login_result = ftp_login($ftp, FTP_USER, FTP_PASS);
 
            // prepneme se do adresare,
            // ve kterem budeme ukladat adresare s fotkami
            $new_dir = ftp_chdir($ftp, DIR_PHOTO_FTP);
 
            // pokud adresar neexistuje, vytvorime
            if(!file_exists(FILEREPOSITORY)) $dir = ftp_mkdir($ftp, date("Y-m-d"));
 
            // nastavime prava na adresar, abychom do nej mohli zapisovat
            ftp_site($ftp, "CHMOD 0777 ".date("Y-m-d"));
 
            // nyni se do vytvoreneho adresare prepneme,
            // abychom do nej mohli fotku ulozit
            $new_dir = ftp_chdir($ftp, date("Y-m-d"));
 
            // ulozime fotku
            $result = ftp_put($ftp, $file_name.".jpg", $_FILES['photo']['tmp_name'], FTP_BINARY);
 
            // prepneme se zpet do nadrazeneho adresare
            $new_dir = ftp_chdir($ftp, "../");
 
            // a zamezime zapisovani pro GROUP (skupina), OTHER (ostatni)
            ftp_site($ftp, "CHMOD 0755 ".date("Y-m-d"));
 
            // uzavreme FTP spojeni
            ftp_close($ftp);
 
            // zjistime, jestli uploadovani probehlo v poradku
            if ($result == true) {
 
              $store_day = date("Y-m-d");
 
              // aby bylo obrazek mozne pozdeji elegantne najit
              // a zobrazit, ulozime zaznam do databaze o jeho ulozeni
              // pro jednoduchost ukladam pouze den ulozeni a jmeno obrazku
              // muzete ulozit treba komentar (pro ALT),
              // cas ulozeni, kdo ho ulozil, pocet zobrazeni apod....
              $db->query("INSERT INTO my_photo (photo_store_day, name_photo) VALUES('$store_day', '$file_name')");
 
              // obrazek byl upesne uploadovan.
              return 200;
 
            } else {
              $file_name = "";
              // Obrázek se nepodařilo vložit
              return 301;
            }
          } else {
            return 302; // Obrázek není ve formátu JPG
          }
        } else {
          return 303; //Obrázek nebyl uploadován
        }
      } else {
        return 304;
      }
 
    } # konec funkce upload_photo

} # konec tridy kloe_functions
?>

Zrovna v tomto příkladu máme jen jednu funkci, která obstarává vše co potřebujeme, takže třída je zbytečná. Osobně upřednostňuji, už jen z důvodu případného rozšiřování a provádění změn.

Příklad použití:

<?php
  // pripojeni pripravenych souboru
  include_once("config.inc.php");
  include_once("class.connect.php");
  include_once("class.functions.php");
 
  $kloe = new kloe_functions();
?>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="cs" lang="cs">
 
<head>
  <title>eKloe.com - upload obrázků</title>
  <meta name="AUTHOR" content="Lukas Jevicky" />
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta name="description" content="eKloe.com - upload obrázků" />
  <meta name="keywords" content="upload, ftp" />
</head>
<body>
 
<?php
 
  if(isset($_POST['upload_photo'])) {
 
    $result = $kloe->upload_photo();
    if($result == 200) echo "Fotka byla úspěšně uploadována";
    if($result == 301) echo "Fotku se nepodařilo vložit";
    if($result == 302) echo "Obrázek není ve formátu JPG";
    if($result == 303) echo "Fotka nebyla uploadována";
 
    if($result == 304) echo "Fotka je příliš veliká";
 
  }
 
?>
 
<form action="index.php" method="POST" enctype="multipart/form-data">
  Vyber fotku: <input type="file" name="photo" value="" />
  <input type="submit" name="upload_photo" value="Vložit novou fotku" />
</form>
 
</body>
</html>

Poznámka: Z pohledu bezpečnosti, je doporučováno všude psát absolutní cesty, včetně includovaných souborů do skriptu. Každý budete mít jinou absoutní cestu, proto doplnění nechávám na Vás a includuji relativně ;-)

V odesílacím formuláři nezapomeňte uvést, že se budou na server posílat i data a to zápisem enctype=”multipart/form-data”.

Pokud budete chtít uložit do databáze záznam o uložení, musíte ještě vytvořit tabulku a ve funkci případně upravit řádek s SQL dotazem. Abyste do dotazu nemuseli zasahovat, použijte následující zápis pro vytvoření tabulky. Připomínám, že se jedná o postgreSQL databázi.

CREATE SEQUENCE my_photo_seq
  INCREMENT 1
  MINVALUE 1
  MAXVALUE 9223372036854775807
  START 1
  CACHE 1;
 
CREATE TABLE my_photo
(
  id_photo bigint NOT NULL DEFAULT NEXTVAL('my_photo_seq'::text),
  photo_store_day date NOT NULL DEFAULT '1900-01-01'::date, -- den ulozeni fotky
  name_photo character(32) NOT NULL DEFAULT 0, -- jmeno fotky
  CONSTRAINT id_photo PRIMARY KEY (id_photo)
)
WITH OIDS;

Vytvořte si i sekvenci k tabulce. Sekvence vpodstatě nahrazují AUTO_INCREMENT u mySQL databází. Automaticky přiřadí unikátní číslo řádku. Lze použít i OIDS (unikátní označení v rámci celé databáze) a sloupec id_photo úplně vypustit. Osobně upřednostňuji ID v rámci tabulky, ale mám zároveň zaplé i OIDS, jak lze vidět v SQL dotazu. Zakáznání OIDS docílíte napsáním WITHOUT OIDS.

Celý příklad si můžete stáhnout zde: Upload obrázku.

Možná rozšíření:

  • Zmenšování obrázku
  • Vložení vodotisku do obrázku

Sdílej:
  • Facebook
  • Google Bookmarks
  • Live
  • Twitter
  • StumbleUpon
:,
Zatím žádný komentář. Buď první :-)

Odpovědět

Hledat: