Archiv pro rubriku: php

Jak NETTE šetří práci

Občas potřebuji udělat jen drbku na webu, který na NETTE neběží a pak nastane otázka, zda to dělat ještě v pure PHP a nebo nějak do toho komponovat NETTE. U mě je odpověď jasná. NETTE!

Zadání:
Přidat na stávající web formulář pro odeslání mailů.

Řešení:
Kdekoliv je libo (někdo plácá HTML s PHP, někdo typy obsahu dělí mezi šablonu a další soubory, …) si vygenerujeme formulář s plnou podporou NETTE.

include_once $_SERVER['DOCUMENT_ROOT'] . '/_libs/nette.min.php';
 
use	Nette\Mail\Message,
	Nette\Forms\Form;
 
// vytvoření formuláře		
$form = new Form;
$form->addText('jmeno', 'Jméno a příjmení *:')
	->setRequired('Zadejte prosím jméno');
$form->addText('mail', 'E-mail:');
$form->addText('tel', 'Telefon:')
	->addConditionOn($form['mail'], Form::EQUAL, '')
		->addRule(Form::FILLED, 'Zadejte prosím alespoň jeden z kontaktů');
 
$form['mail']->addConditionOn($form['tel'], Form::EQUAL, '')
	->addRule(Form::FILLED, 'Zadejte prosím alespoň jeden z kontaktů')
	->addRule(Form::EMAIL, 'E-mail nemá správný formát');
 
$form->addSelect('zajem', 'Mám zájem o:', array('sádrokartony, rekonstrukce' => 'sádrokartony, rekonstrukce', 'malby, nátěry' => 'malby, nátěry', 'ostatní' => 'ostatní'));
$form->addTextArea('pozn', 'Poznámka:', 40, 4);
 
// antispam
// podrobněji toto řešení rozebírám v jiném článku
$c1 = date('j')+3;
$c2 = date('N')+2;
$s = $c1 + $c2;
$form->addText('soucet', sprintf('Zadejte součet %s+%s *:', $c1, $c2))
	->setRequired('Je třeba zadat součet jako ochranu proti SPAMu')
	->addRule(Form::EQUAL, 'Je třeba zadat součet jako ochranu proti SPAMu', $s);
$form->addHidden('c1', $c1);
$form->addHidden('c2', $c2);
$form->addSubmit('send', 'Odeslat');

Pak kdekoliv chci formulář zobrazit tak napíši jen:

echo $form;

Pro JS validaci (není nutná) stačí nahrát a nalinkovat soubor netteForm.js:

<script type="text/javascript" src="../_js/netteForms.js"></script>
 
<script type="text/javascript">
	// řešení pro antispam popisuji jinde
	document.getElementById("frm-soucet").value = parseInt(document.getElementById("frm-c1").value) + parseInt(document.getElementById("frm-c2").value);
	document.getElementById("frm-soucet").parentNode.parentNode.style.display = "none";
</script>

Zpracování formuláře a odeslání ho na mail jde stejně jednoduše:

if ($form->isSuccess()) {
	$val = $form->getValues();
	$mail = new Message;
 
	$from = ($val['mail'] == '') ? 'Z webu <web @nejaky-web.cz>' : $val['jmeno'] . ' < ' . $val['mail'] . '>';
	$mail->setFrom($from)
		->addTo('Info <info @nejaky-web.cz>')
		->setSubject('Poptávka z webu')
		->setBody(sprintf("Dobrý den,\nJméno: %s\nE-mail: %s\nTelefon: %s\nZájem o: %s\nPoznámka:\n%s", $val['jmeno'], $val['mail'], $val['tel'], $val['zajem'], $val['pozn']))
		->send();
}</info></web>

Jak dlouho byste toto dělali v čistém PHP?

Jak neotravovat uživatele s antispamem? – realizace s podporou NETTE

V článku jsem nastínil, jak chránit webový formulář antispamem bez zbytečné interakce uživatelů.

Jak to ale propojit s NETTE a jeho generací formulářů.

Příklad je napsán bez použití kompletního frameworku. Používám NETTE jen jako knihovnu.

PHP:

include_once $_SERVER['DOCUMENT_ROOT'] . '/_libs/nette.min.php';
 
use	Nette\Mail\Message,
	Nette\Forms\Form;
 
// vytvoření formuláře		
$form = new Form;
$form->addText('jmeno', 'Jméno a příjmení *:')
	->setRequired('Zadejte prosím jméno');
$form->addText('mail', 'E-mail:');
$form->addText('tel', 'Telefon:')
	->addConditionOn($form['mail'], Form::EQUAL, '')
		->addRule(Form::FILLED, 'Zadejte prosím alespoň jeden z kontaktů');
 
$form['mail']->addConditionOn($form['tel'], Form::EQUAL, '')
	->addRule(Form::FILLED, 'Zadejte prosím alespoň jeden z kontaktů')
	->addRule(Form::EMAIL, 'E-mail nemá správný formát');
 
$form->addSelect('zajem', 'Mám zájem o:', array('sádrokartony, rekonstrukce' => 'sádrokartony, rekonstrukce', 'malby, nátěry' => 'malby, nátěry', 'ostatní' => 'ostatní'));
$form->addTextArea('pozn', 'Poznámka:', 40, 4);
 
// antispam
// Nelze řešit přes RAND, jelikož po odeslání by se nám hodnoty změnily, toto beru jako zajímavé "konstanty", které mají platnost jeden den.
// Nápady jak toto lépe řešit uvítám v diskuzi.
$c1 = date('j')+3;
$c2 = date('N')+2;
$s = $c1 + $c2;
$form->addText('soucet', sprintf('Zadejte součet %s+%s *:', $c1, $c2))
	->setRequired('Je třeba zadat součet jako ochranu proti SPAMu')
	->addRule(Form::EQUAL, 'Je třeba zadat součet jako ochranu proti SPAMu', $s);
$form->addHidden('c1', $c1);
$form->addHidden('c2', $c2);
$form->addSubmit('send', 'Odeslat');
 
// zpracování formuláře po odeslání
if ($form->isSuccess()) {
	$val = $form->getValues();
	$mail = new Message;
 
	$from = ($val['mail'] == '') ? 'Z webu <web @nejaky-web.cz>' : $val['jmeno'] . ' < ' . $val['mail'] . '>';
	$mail->setFrom($from)
		->addTo('Info <info @nejaky-web.cz>')
		->setSubject('Poptávka z webu')
		->setBody(sprintf("Dobrý den,\nJméno: %s\nE-mail: %s\nTelefon: %s\nZájem o: %s\nPoznámka:\n%s", $val['jmeno'], $val['mail'], $val['tel'], $val['zajem'], $val['pozn']))
		->send();
 
	// nesouvisí s příkladem - vložíme poděkování
	include $_SERVER['DOCUMENT_ROOT'] . '/templates/formular-odeslan.php';
} else {
	echo $form;
}

Vypíchnu raději bokem samotnou podstatu antispamového řešení:

// antispam
// Nelze řešit přes RAND, jelikož po odeslání by se nám hodnoty změnily, toto beru jako zajímavé "konstanty", které mají platnost jeden den.
// Nápady jak toto lépe řešit uvítám v diskuzi.
$c1 = date('j')+3;
$c2 = date('N')+2;
$s = $c1 + $c2;
$form->addText('soucet', sprintf('Zadejte součet %s+%s *:', $c1, $c2))
	->setRequired('Je třeba zadat součet jako ochranu proti SPAMu')
	->addRule(Form::EQUAL, 'Je třeba zadat součet jako ochranu proti SPAMu', $s);
$form->addHidden('c1', $c1);
$form->addHidden('c2', $c2)

Obslužný JS, který nám v případě zapnutého JS skryje antispamový prvek

<script type="text/javascript">
	document.getElementById("frm-soucet").value = parseInt(document.getElementById("frm-c1").value) + parseInt(document.getElementById("frm-c2").value);
	document.getElementById("frm-soucet").parentNode.parentNode.style.display = "none"; // musíme se dostat až ke značce TR
</script>

Jak na multiselect v NETTE + dibi

Potřeboval jsem použít multiselectu v e-shopu, který právě dělám, pro určení souvisejícího zboží a narazil jsem na pár zádrhelů, které zde popíši.

1. Jak naplnit multiselect daty, při použití skupin
Toto má být výsledek
Multiselect

Je třeba dodat data ve formátu:

array (
"Bombóny" => array (
   15 => "Hašlerky"
   16 => "Bompary"
),
"Lízátka" => array (
   11 => "Chupachups"
),
"Čokolády" => array (
   12 => "Milka"
   17 => "Orion"
),
"Ostatní" => (
   14 => "Špalet"
   18 => "Pendrek"
)
)

Jak tento formát získat záleží zejména na struktuře databáze. Předpokládám, že pokud je „ideální“, tak by to šlo pomocí vhodné asociace (viz příklad níže). Vzhledem k tomu, že moje databáze má jinou strukturu a kýženému výsledku jsem se musel dobrat mnohem složitěji (iterace nad daty, dotahování dalších dat pomocí dalších selectů, rekurze, …), tak následující příklad je jen myšlenka, jak by to mohlo fungovat (chybí tam například výběr IDcek a následující nutné UNSETy).

   $res = dibi::query('SELECT [nazev_menu], [nazev_produktu] FROM [menu] JOIN [zbozi] USING ([kod_menu]) ORDER BY [nazev_menu], [nazev_produktu]');
   $res->fetchAssoc('nazev_menu', '=', 'nazev_produktu');

Po získání dat stačí již jednoduše zavolat:

    $form->addMultiSelect('items', 'Související produkty:', $items);

2. Vybrání default hodnot
NETTE očekává pro formulářový prvek multiselect formát:

    array(id1, id2, id3);

Ten získáme malým trikem pomocí fetchPairs (příklad mojí konkrétní funkce z modelu):

    public function getRelated($id)
    {
        return $this->connection->select('id_2')
	                        ->from(self::TABLE_PAGES_RELATED)
	                        ->where('id_1=', $id)
	                        ->fetchPairs('id_2', 'id_2'); // tento řádek je ten důležitý
    }

A následně v presenteru:

	$model = new RelatedModel;
	$items = $model->getRelated($id);
 
	$form = $this->getComponent('itemsForm');
	$form->setDefaults(array('items'=> $items));

3. Uložení získaných dat
NETTE nám z formuláře vrátí data ve formátu:

"items" => array (
   0 => "15"
   1 => "11"
)

ale takto je nelze předhodit SQL dotazu pro INSERT a musím je tedy přetransformovat do tvaru ID=>ID. Což vypadá ve výsledku takto:

"items" => array (
   array (
      "id_1" => "14",
      "id_2" => "15"
   ),
   array (
      "id_1" => "14",
      "id_2" => "11"
   )
)

kde id_1 je id editovaného záznamu a id_2 jsou související id (ze stejné tabulky jako id_1).
Takový formát se již dá předhodit dibi. Konkrétní funkce z mého modelu vypadá následovně:

    public function setRelated($id, $data)
    {
	dibi::query('DELETE FROM [pages_related] WHERE [id_1]=%i', $id); // vyčistíme si data pro dané ID
	dibi::query('INSERT INTO [pages_related] %ex', $data); // uložíme si nové související položky. %ex provede expanzi pole pro multiinsert		
    }

Zde je i ukázáno jakým způsobem provést multiinsert dat, aby nebylo nutno iterovat nad daty a provádět jednotlivé inserty.

A nyní už chybí jen kus kódu, který transformuje data na výše popsaný formát. pro přehlednost ukážu celou metodu pro zpracování formulářových dat.

    public function itemsFormSubmitted($form)
    {
		$data = $form->getValues();
		$model = new RelatedModel;
 
		$arr = array();
		foreach($data['items'] as $key => $value) {
			$arr[$key]['id_1'] = $data['id'];
			$arr[$key]['id_2'] = $value;
		}
		$model->setRelated($data['id'], $arr);
 
		$this->flashMessage('Byly aktualizovány související produkty.', 'info');
		$this->redirect('this');
	}

Vše!

Autentizace (přihlášení) v NETTE pomocí LDAP – omezení uživatelů

Navazuji na předchozí návod. Ale navíc jsem řešil, jak co nejjednodušeji zajistit, aby do aplikace mohla jen určitá podmnožina uživatelů z LDAP (u mě konkrétně Active Directory).
Možnosti:

  • Využít vlastnosti struktury či atributů Active Directory – u mě nepřipadá v úvahu
  • Mít další evidenci v databázi webové aplikace. Například s INDEXem na atribut $username. – Zbytečná evidence navíc, které se musí spravovat.
  • Mít ve výčtu povolené $username.
    	$allow_usernames = array('honza', 'mojmir', 'patrik');

    A dále kontrolovat, zda přihlašovaný je v tomto poli. – Evidence navíc a při změně oprávnění je třeba magické úpravy v aplikační logice.

  • Přidat kontrolní heslo, které budou znát jen administrátoři. – Zdá se, že je zbytečná další vazba na Active Directory, proč prostě nestačí toto admin heslo, ale pokud vypnu uživatele v Active Directory, tak automaticky zamezím přístup i do administrace, ale bez této kombinace by to nešlo.

Authenticator:

< ?php
// pro PHP 5.3
// use Nette\Object, Nette\Security\IAuthenticator, Nette\Security\AuthenticationException, Nette\Security\Identity;
 
class MyAuthenticator extends Object implements IAuthenticator {
 
	public function authenticate(array $credentials) {
		$username = $credentials[self::USERNAME];
		$password = $credentials[self::PASSWORD];
		$control_password = $credentials['extra'];
 
		$ldap_conn = ldap_connect ('ldap://server.domena.cz');
		if ($ldap_conn) {
			$ldapbind = @ldap_bind ($ldap_conn, $username . '@domena.tld', $password);
			ldap_unbind ($ldap_conn);
			if ($ldapbind) {
				if ($control_password === 'nejake_zvolene_kontrolni_heslo') {
					return new Identity('Správce.', 'Administrátor', $ldapbind);
				} else {
					throw new AuthenticationException("Účet nemá dostatečná práva.", self::NOT_APPROVED);
				}
			} else {
				throw new AuthenticationException("Špatné jméno nebo heslo.", self::INVALID_CREDENTIAL);
			}
		} else {
			throw new AuthenticationException("Ověřovací server není dostupný.", self::FAILURE);
		}
	}
}

Použití v presenteru (zatím ve stínu starého NETTE):

	protected function createComponentLogin()
	{
		$form = new AppForm;
		$form->addText('login', 'Přihlašovací jméno:')
			->addRule(Form::FILLED, 'Pole "Přihlašovací jméno" je povinné.');
		$form->addPassword('password', 'Heslo:')
			->addRule(Form::FILLED, 'Pole "Heslo" je povinné.');
		$form->addPassword('control', 'Kontrolní heslo:')
			->addRule(Form::FILLED, 'Pole "Kontrolní heslo" je povinné.');
        	// Obrana proti CSRF
		$form->addProtection('Vypršel čas pro přihlášení.');
		$form->addSubmit('send', ' Přihlásit ');
 
		$form->onSubmit[] = array($this, 'formLoginSubmitted');
 
		$this->addComponent($form, "login");
	}
 
	function formLoginSubmitted($form) {
		$user = Environment::getUser();
		$user->setExpiration('+ 15 minutes');
		$values = $form->getValues();
 
		try {
			// pokusíme se přihlásit uživatele...
			// tady je to nejdůležitější, třetí parametr LOGINu
			$user->login($values['login'], $values['password'], $values['control']);
			// ...a v případě úspěchu přesměrujeme na další stránku
			$this->flashMessage('Přihlášení proběhlo úspěšně.');
			$this->redirect('Admin:default');
		} catch (AuthenticationException $e) {
			$this->flashMessage($e->getMessage(), 'validation');
		}
	}

Autentizace (přihlášení) v NETTE pomocí LDAP

Ne, že by to bylo něco složitého, pro někoho kdo má s LDAP již nějaké zkušenosti, ale myslím, že komplexní návod se hodí.

< ?php
// pro PHP 5.3
// use Nette\Object, Nette\Security\IAuthenticator, Nette\Security\AuthenticationException, Nette\Security\Identity;
 
class MyAuthenticator extends Object implements IAuthenticator {
 
	public function authenticate(array $credentials) {
		$username = $credentials[self::USERNAME];
		$password = $credentials[self::PASSWORD];
 
		$ldap_conn = ldap_connect ('ldap://overovaci.server.cz');
		if ($ldap_conn) {
			$ldapbind = @ldap_bind ($ldap_conn, $username . '@domena.tld', $password);
			ldap_unbind ($ldap_conn);
			if ($ldapbind) {
				return new Identity('Správce.', 'Administrátor', $ldapbind);
			} else {
				throw new AuthenticationException("Špatné jméno nebo heslo.", self::INVALID_CREDENTIAL);
			}
		} else {
			throw new AuthenticationException("Ověřovací server není dostupný.", self::FAILURE);
		}
	}
}

Pro jednoduchost se v tomto příkladu neřeší další správa rolí, nezjišťují se podrobnosti o uživateli atd. Možná jindy…