Archiv rubriky: php

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…

Jak v NETTE změnit řazení tabulky klikem na záhlaví sloupců

Klasická situace, ale není to tak jednoduché jak se zdá. Co všechno musíme řešit?

  1. podle kterého sloupce řadíme
  2. jak je naposledy seřazen

Pokud toto už máme napsáno, tak dále musíme řešit různé překliky mezi sloupci.
Jdeme na věc!
Presenter:

< ?php
class AdminPresenter extends AdminBasePresenter {
 
	/** @persistent int */ // označuje proměnou níže jako persistentní - proměnná se sama přenáší mezi kliky v rámci presenteru (náhrada bezestavovosti protokolu HTTP)
	public $p_sort = 1; //  proměnná pro uložením posledního řazeného sloupce
 
	/* 
	 * $sort Integer - pořadové číslo sloupce z dotazu podle kterého se má řadit
	 * $by String - způsob řazení
	 */ 
	public function renderTabulka($sort = 1, $by = 'ASC') { 
		$by = ($sort != $this->p_sort) ? 'ASC' : $by; // zjistíme jestli řadíme stejný sloupec jako minule
		$this->template->rows = dibi::fetchAll('SELECT ... FROM ... ORDER BY %i %sql', $sort, $by); // seřadíme
		$this->template->by = ($by == 'ASC') ? 'DESC' : 'ASC'; // pošleme proměnou s opačným způsobem řazení do templatu 
		$this->p_sort = $sort; // uložíme sloupec, podle kterého jsme řadili
	}

Template:

<table>
<tr>
	<td><a href="{plink sklad, 1, $by}">Sloupec 1</a></td>
	<td><a href="{plink sklad, 2, $by}">Sloupec 2</a></td>
	<td><a href="{plink sklad, 3, $by}">Sloupec 3</a></td>
</tr>
{if count($rows) > 0}
	{foreach $rows as $row}
		<tr>
			<td> {$row['sloupec1']} </td>
			<td> {$row['sloupec2']} </td>
			<td> {$row['sloupec3']} </td>
		</tr>
	{/foreach}
{else}
    <tr><td colspan="3">V databázi se nenalézá žádný záznam.</td></tr>
{/if}
</table>

Doufám, že to je pochopitelné :).

Jak se připojit z PHP pomocí CURL k SSL (HTTPS) adrese

K zaslání klasického požadavku stačí následující kód:

if ($curl = curl_init(ADRESA)) {
	curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
	$content = curl_exec($curl);
	curl_close($curl);
}

Ale pokud je adresa chráněna pomocí SSL (začíná https://), tak tento kód vrací chybu:
SSL certificate problem, verify that the CA cert is OK. Details:
error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed
.
Řešení je jednoduché. Stačí přidat řádek:
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);.
Takže výsledný kód vypadá následovně:

if ($curl = curl_init(ADRESA)) {
	curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
	curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
	$content = curl_exec($curl);
	curl_close($curl);
}

SPRÁVNÉ ŘEŠENÍ PRO: Přímé stažení souboru v NETTE pomocí redirectu formuláře

Již dříve jsem nastínil jak na to. Ale to bylo řešení postavené na dříve osvojených principech (si všechno napsat sám) a až nyní jsem našel způsob, jak to napsat elegantněji pomocí NETTE.

Presenter:

< ?php
 
class PcClientPresenter extends BasePresenter
{
	protected function createComponentForm($name) {
		$form = new AppForm($this, $name);
		$form->addPassword('heslo', 'Heslo: ')
			->addRule(Form::FILLED, 'Heslo musí být vyplněné.');
		$form->addSubmit('send', 'Stáhnout soubor »');
 
		$form->onSubmit[] = array($this, 'formSubmitted');
	}
 
	public function formSubmitted($form) {
		$file = WWW_DIR . '/data/soubor.zip'; // soubor může být úplně mimo web root (=nestáhnutelný pomocí URL)
		$fileName = 'Soubor po stažení.zip'; // název pod kterým se bude soubor stahovat uživateli
 
		$arr = $form->getValues();
		if ($arr['heslo'] === 'HESLO') {
			$httpResponse = Environment::getHttpResponse();
			$httpResponse->setHeader('Pragma', "public");
			$httpResponse->setHeader('Expires', 0);
			$httpResponse->setHeader('Cache-Control', "must-revalidate, post-check=0, pre-check=0");
			$httpResponse->setHeader('Content-Transfer-Encoding', "binary");
			$httpResponse->setHeader('Content-Description', "File Transfer");
			$httpResponse->setHeader('Content-Length', filesize($file));
			$this->sendResponse(new DownloadResponse($file, $fileName, array('application/octet-stream', 'application/force-download', 'application/download')));
		} else {
			$this->flashMessage('Nesouhlasí zadané heslo.', 'error');
		}
	}
 
	public function actionDefault() {}
}

Pro pořádek připojuji i obsah šablony:

{block #content}
<h1>Stažení PC klienta</h1>
    <div class="flash {$flash->type}">{$flash->message}</div>
    {control form}
{/block}

Přímé stažení souboru v NETTE pomocí redirectu formuláře

Potřeboval jsem udělat jednoduché stahovadlo souboru. Požadavky byly, aby pro stažení se muselo zadat heslo a aby soubor nešlo ani následně stahovat přes nějakou konstantní URL.

Níže uváděné řešení již považuji za zastaralé a nechávám ho tady jen aby bylo možné porovnat, že i toto jde v NETTE napsat lépe. Pokračujte zde…


Řešení je nakonec triviální:
Presenter:

< ?php class PcClientPresenter extends BasePresenter {
 	protected function createComponentForm($name) {
 		$form = new AppForm($this, $name);
 		$form->addPassword('heslo', 'Heslo: ')
			->addRule(Form::FILLED, 'Heslo musí být vyplněné.');
		$form->addSubmit('send', 'Stáhnout soubor »');
 
		$form->onSubmit[] = array($this, 'formSubmitted');
	}
 
	public function formSubmitted($form) {
		$file = WWW_DIR . '/data/soubor.zip'; // soubor může být úplně mimo web root (=nestáhnutelný pomocí URL)
		$fileName = 'Soubor po stažení.zip'; // název pod kterým se bude soubor stahovat uživateli
 
		$arr = $form->getValues();
		if ($arr['heslo'] === 'HESLO') {
			$httpResponse = Environment::getHttpResponse();
			// Pošleme prohlížeči hlavičky pomocí kterých mu řekneme že se jedná o stahování
			$httpResponse->setContentType('application/octet-stream');
			$httpResponse->setContentType('application/force-download');
			$httpResponse->setContentType('application/download');
			$httpResponse->setHeader('Pragma', "public");
			$httpResponse->setHeader('Expires', 0);
			$httpResponse->setHeader('Cache-Control', "must-revalidate, post-check=0, pre-check=0");
			$httpResponse->setHeader('Content-Transfer-Encoding', "binary");
			$httpResponse->setHeader('Content-Description', "File Transfer");
			$httpResponse->setHeader('Content-Length', filesize($file));
			$httpResponse->setHeader('Content-Disposition', 'attachment; filename="' . $fileName . '"');
 
			// v cyklu načítáme postupně soubor a posíláme ho prohlížeči (u malých souborů by to šlo najednou ale u velkých by došla dostupná paměť pro PHP)
			$fp = fopen($file, 'r');
			while (!feof($fp)) {
				echo fread($fp, 65536);
				flush();
			}
			fclose($fp);
			$this->terminate();
		} else {
			$this->flashMessage('Nesouhlasí zadané heslo.', 'error');
		}
	}
 
	public function actionDefault() {}
}

Template:

{block #content}
<h1>Stažení PC klienta</h1>
<div class="flash {$flash->type}">{$flash->message}</div>
{control form}
{/block}


Vše!

Jak se odkazovat na předchozí a následující příspěvek

V tomto příspěvku mi nejde o klasické stránkování, které se dá snadno vyřešit pomocí klauzule LIMIT v MySQL.
Nejde mi ani o seznamy, které se dají jednoduše a striktně seřadit podle velikosti (číslo, čas), ale jde mi o případ, kdy máme například seznam článků seřazených podle jména a chceme jednoduše přecházet na další či předchozí. Tady řazení podle operátorů <, > nebude to pravé.
Vyřešit se to dá následovně (vykopírováno z NETTE aplikace s použitou knihovnou DIBI):

// funkce zobrazuje objednávku dle ID, pokud ji pošleme parametr KAM, tak se zpracuje a funkce se zavolá znova jen s konkrétní ID
 
public function renderDetail($id, $kam = '') {
	if ($kam == '') {
		$this->template->rows = dibi::fetchAll('SELECT * FROM [objednavka_polozky] [o] JOIN [w_zbozi] USING ([kod_zbozi]) JOIN [objednavka_hlavicka] USING ([id_objednavka]) WHERE [id_objednavka] = %i GROUP BY [kod_zbozi]', $id);
	} elseif ($kam == 'PREV') {
		dibi::query('SET @poradi:=0, @poradinow:=0, @idprev:=0, @idnext:=0;');
		dibi::query('SELECT @poradi:=@poradi+1, IF(id_objednavka=%i, @poradinow:=@poradi, 0) FROM [objednavka_hlavicka] [oh] ORDER BY [cas] DESC', $id);
		dibi::query('SET @poradi:=0;');
		dibi::query('SELECT @poradi:=@poradi+1, IF(@poradinow-1=@poradi, @idprev:=id_objednavka, 0), IF(@poradinow+1=@poradi, @idnext:=id_objednavka, 0) FROM [objednavka_hlavicka] [oh] ORDER BY [cas] DESC');
		$row = dibi::fetch('SELECT @poradinow, @idnext AS next, @idprev AS prev;');
		if ($row['prev'] == 0) {
			$this->flashMessage('Nelze se již posunout na předchozí objednávku.', 'info');
			$this->redirect('Admin:detail', array('id' => $id));
		}
		$this->redirect('Admin:detail', array('id' => $row['prev']));
	} elseif ($kam == 'NEXT') {
		dibi::query('SET @poradi:=0, @poradinow:=0, @idprev:=0, @idnext:=0;');
		dibi::query('SELECT @poradi:=@poradi+1, IF(id_objednavka=%i, @poradinow:=@poradi, 0) FROM [objednavka_hlavicka] [oh] ORDER BY [cas] DESC', $id);
		dibi::query('SET @poradi:=0;');
		dibi::query('SELECT @poradi:=@poradi+1, IF(@poradinow-1=@poradi, @idprev:=id_objednavka, 0), IF(@poradinow+1=@poradi, @idnext:=id_objednavka, 0) FROM [objednavka_hlavicka] [oh] ORDER BY [cas] DESC');
		$row = dibi::fetch('SELECT @poradinow, @idnext AS next, @idprev AS prev;');
		if ($row['next'] == 0) {
			$this->flashMessage('Nelze se již posunout na následující objednávku.', 'info');
			$this->redirect('Admin:detail', array('id' => $id));
		}
		$this->redirect('Admin:detail', array('id' => $row['next']));
	} else {
		//chyba
		$this->redirect('Admin:objednavky');
	}
}

Příklad není úplně přehledný, protože jsem ho vykopíroval z hotové aplikace a aplikační logika není úplně čistá, ale pro demonstraci to doufám stačí.

Automatické dotahování údajů o podnikatelích z databáze ARES

Někde na internetu jsem viděl jak po zadání IČ se zbytek údajů o živnostníkovi/firmě dotáhl sám. A jelikož jsem si chtěl o tuto možnost rozšířit i své personální účetnictví, tak jsem pátral jak na to.

O databázi ARES jsem se dozvěděl již dříve, ale nikdy jsem se nedostal k samotné implementaci. Až dneska jsem narazil na článek Radka Hulána, řešící přesně toto a tak jsem si řekl, že se na to podívám.

Zdroj dat: Databáze ARES
Script: MyEGO blog

Script po úpravě na straně serveru (jedná se o NETTE akci):

public function handleLoadInfo($IC) {
	$this->payload->firma = array();
    	// dá se vybrat hned z několika zdrojů dle potřeby http://wwwinfo.mfcr.cz/ares/ares_xml.html.cz#k3
	define('ARES','http://wwwinfo.mfcr.cz/cgi-bin/ares/darv_bas.cgi?ico=');
	$ico = intval($IC);
	// nemohl jsem použít kvůli omezení na serveru, nahradil jsem pomocí CURL
	//$file = @file_get_contents(ARES.$ico);
	if ($curl = curl_init(ARES.$ico)) {
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
		$content = curl_exec($curl);
		//$info = curl_getinfo($curl);
		curl_close($curl);
		$xml = @simplexml_load_string($content);
	}
	$a = array();
	if (isset($xml)) {
		$ns = $xml->getDocNamespaces();
		$data = $xml->children($ns['are']);
		$el = $data->children($ns['D'])->VBAS;
		if (strval($el->ICO) == $ico) {
			$a['ico'] 	= strval($el->ICO);
			$a['dic'] 	= strval($el->DIC);
			$a['firma'] = strval($el->OF);
			$a['ulice']	= strval($el->AA->NU).' '.strval($el->AA->CD).'/'.strval($el->AA->CO);
			$a['mesto']	= strval($el->AA->N).'-'.strval($el->AA->NCO);
			$a['psc']	= strval($el->AA->PSC);
			$a['stav'] 	= 'ok';
		} else {
			$a['stav'] 	= 'IČ firmy nebylo nalezeno';
		}
	} else {
		$a['stav'] 	= 'Databáze ARES není dostupná';
	}
	$this->payload->firma = $a;
	$this->sendPayload();
}

Script po úpravě na straně uživatele:

<script type="text/javascript">
    <!--
	$('#frmkontakt-IC').change(function(event) {
		$.getJSON({link loadInfo!}, {'IC': $('#frmkontakt-IC').val()}, function(payload) {
			if (payload.firma.stav == 'ok') {
				$('input[name=DIC]').val(payload.firma.dic);
				$('input[name=prijmeni]').val(payload.firma.firma);
				$('input[name=ulice]').val(payload.firma.ulice);
				$('input[name=mesto]').val(payload.firma.mesto);
				$('input[name=psc]').val(payload.firma.psc);
			} else {
				alert(payload.firma.stav);
			}
		});
	});
	-->
</script>

Chtělo by to ještě doladit o podmínky kdy co a jak vypisovat, spojovat (číslo orientační a popisné, město a čtvrť, atd.), ale jako referenční implementace to je dobrý základ.

Jak na potvrzení registrace v NETTE?

U jednoho dříve zveřejněného příspěvku (Registrace uživatelů pomocí NETTE) se objevil komentář žádající vysvětlení, jak dále zpracovávat potvrzující odkaz z došlého mailu.
Pro nevytržení z kontextu zopakuji část, která takový odkaz tvoří a odesílá e-mail. Kód jsem upravil o dynamickou tvorbu odkazu, jak jsem byl upozorněn v diskuzi u dříve zmíněného článku.

// vytvoření kontrolního hashe
$kontrolni_link = sha1($values['nick'] . time() . 'e743dd075ff52c2');
 
unset($values['pass2']); // druhé heslo neukládáme
$values['retik'] = $kontrolni_link;
dibi::query('INSERT INTO [user]', $values);
 
#mail pro kontrolu
$mail = new Mail;
$mail->setFrom('Registrace na webu xxx.com <info @xxx.com>');
$mail->addTo($values['email']);
$mail->setSubject('Registrace na webu xxx.com');
$mail->setBody("Děkujeme za Váši registraci. Potvrďte prosím na adrese " . $this->link('//Registrace:kontrola', array('kontrola'=>$kontrolni_link)) . ".");
$mail->send();
// zase mi tu WP zlobí dále už nic nemá být ;-)
</info>

Toto je jen jedna možnost jak kontrolu dělat. Díky způsobu tvorby (používáme funkci time() a její hodnotu si nikam dále neukládáme) kontrolního HASHe, je nutné ho uložit do databáze a následně při kontrole ho testovat zda odpovídá. Jen pro informaci zmíním, že je tu i další možnost tvorby a to, že si odkazem pošleme více parametrů, díky kterým budeme schopni registraci potvrdit, bez dopomocí databáze (např.: $this->link(‚//Registrace:kontrola‘, array(‚nick’=>$values[‚nick‘], ‚kontrola’=>sha1($values[‚nick‘] . ‚e743dd075ff52c2‘)))).

A nyní samotné MINIMALISTICKÉ (=jedná se o koncept, ne o deklaraci dokonalého zpracování) zpracování (budu se držet první možnosti):

if (empty ($_GET['kontrola'])) {
	die('CHYBA!');
} else {
	$kod = $_GET['kontrola'];
	dibi::query('UPDATE [user] SET [retik]="" WHERE [retik]=%s', $kod);
 
	if (dibi::affectedRows() == 1) {
		echo 'Registrace dokončena.';
	} else {
		echo 'Registraci se nepodařilo dokončit.';
	}
}

Nutno podotknout, že při přihlašování mám podmínku na nevyplněnost sloupce retik v tabulce user.

...
$row = dibi::fetch('SELECT [id], [nick], [pass], [retik], [role] FROM [user] WHERE [nick]=%s', $username);
 
if ($row->retik !== '') {
    throw new AuthenticationException("Registrace nebyla dosud potvrzena.", self::NOT_APPROVED);
}
...

Celá ukázka nevyužívá plné síly NETTE. Ukázka vznikla jako návaznost na původní článek, kde se NETTE použilo v již hotové aplikaci. Doufám ale, že postup je z popisu jasný a přepsání do plného NETTE nebude činit problém.

Antispamová ochrana emailu proti robotům

Viděl jsem spousty řešení od nepěkných adres ve formátu „jmeno  <ZAVINAC> domena <TECKA> cz“ nebo kde místo zavináče byl obrázek či vypsání celého mailu pomocí GD knihovny do obrázku a další technoidní masturbace.

Já mám zkušenost, že stačí zavináč a tečku vyměnit za entitu a zatím jsem na žádném projektu neměl problémy. Jasně netvrdím, že to je dokonalé řešení, ale pro 99% vývojářů v PHP je toto řešení dostatečné.

Funkce pro ošetření mailu vypadá takto:

function safeEmail ($email) {
    return str_replace(array('@', '.'), array('&#64;', '&#46;'), $email);
}

Použití:

$email = safeEmail($email);
echo "<a href='mailto:{$email}'>{$email}</a>";

Krásně jednoduché a zároveň funkční bez dopadu na uživatelský komfort.