Dragă programatorule, dacă îți vine a copy-paste, fă refactor!

M-am săturat de cod prost scris ca de mere pădurețe. Nu mă refer la cod nefuncțional sau cu bug-uri. Mă refer la metodologia copy-paste la care se apelează intensiv din când în când. După care apucă-te și modifică ceva pentru a adăuga chestii noi. M-am săturat de gândire non-DRY datorită căreia apuc să modific în 5 locuri și 3 fișiere pentru a pune o chestie amărâtă care să arate la fel peste tot. În concluzie, pe lângă defularea de mai sus, m-am hotărât să mai dau niște idei.

Pe alocuri plângerile mele au avut succes. Acum două zile colegii de echipă mă ascultau în timp ce modificam niște chestii, iar involuntar am zis: iar de aici copiez dincolo … touche: “Ce-ai zis mă? Să copiezi?”. Exact ce ziceam mai sus … câteodată și mie îmi vine greu să nu scriu cod prost. Dar eforturile susținute = evoluție. În concluzie am luat linia aceea lungă (un apel înlănțuit de proceduri) și am pus-o într-o nouă metodă.

În concluzie vreo câteva idei, departe de a oferi o imagine completă:

  • dacă îți vine să faci copy-paste, fie ele și 3 linii de cod sau una lungă, înseamnă ca ai nevoie de un mic refactor.
  • o arhitectură bună, modulară, a aplicației, DRY (și preferabil KISS) compliant, duce la o mentenanță mai ușoară. Pentru a modifica ceva nu este nevoie să cauți toate instanțele aceleiași bucăți de cod.
  • dacă acea parte de ‘unknown’ umbrește puterea de a-ți crea arhitectura înainte de a o implementa, atunci orice model repetitiv din cod stă bine într-o metodă separată.
  • caută să înțelegi framework-ul pe care îl folosești. De exemplu în dezvoltarea web folosind MVC, nu prea are ce căuta într-un controller o chestie ce ar sta bine într-un helper/bibliotecă, pentru că atunci când este nevoie să fie apelată bucata respectivă din alt controller, fără refactor, o să fie trist. Desigur, excepție fac acele controllere moștenite, dar și acolo este o linie fină între ce se poate moșteni și ce ar trebui să fie apelabil global.
  • refactor, OOP, clase, interfețe, ‘design pattern’ (exemplu: singleton) ar trebui să nu fie doar cuvinte într-un vocabular de specialitate.

Importanța unui PHP framework bine scris

Spuneam la GeekMeet-ul din octombrie (damn it, iarăși fac referință la el) în timpul prezentării mele, pentru cei absenți, o abordare de la OS până la coder în privința Web Security, despre importanța folosirii unui framework ce să facă în mod implicit filtrare XSS și SQL Injection (SQLi) a input-ului, în biblioteca ce se ocupă de baza de date. Iar pentru chestii riscante, chiar o filtrare XSS cu HTML Purifier, o bibliotecă a cărui filtru XSS a fost creat să treacă de XSS (Cross Site Scripting) Cheat Sheet.

Știu, nu vreau să fac “carieră” din astfel de post-uri pe blog și nu prea cred că o să mă vedeți vreodată să trimit chestii către http://hackersblog.org/. Sunt preocupat de a proteja, de a-i învăța pe alții cum să se protejeze, și mai puțin de a demonstra vulernabilități. Sunt orientat către ce nu ar trebui să facă aplicația și mai puțin înspre a demonstra cum ajungi acolo. De altel aș prefera ca lumea să nu se ia de ‘junk’-ul de WordPress ce rulează pe blog, sunt prea ocupat pentru a scrie un engine sigur cu un număr echivalent de facilități. Mă rog, va urma un contra-exemplu pentru a susține cele din paragraful anterior.

Povestea începe de la faptul că am avut o mică discuție cu necenzurat despre importanța folosirii unui framework, dar el o susținea pe a lui cu alea 10 kile de cod. Doar pentru ce îți trebuie un framework de 1.25 megi (dimensiunea minimă, maxim 1.49) pentru o aplicație banală? Pai în primul rând bune sunt cele de PHP5 folosind OOP și clase cu auto-load. Poți face multiple instalări folosind aceleași surse ale framework-ului exceptând aplicația în sine. Folosești ce încarci. A da, și ai filtrare implicită. De ce? Pentru că nimeni nu este perfect. Greșeli apar și în codul programatorilor ce au trecut de fazele tatonării. Vorbesc din experiența lucrului și mai mult din experiența lucrului în echipă. Dacă tu ca programator nu o dai de gard, se prea poate să dea altul.

Acu recunosc, am trișat puțin. M-am uitat puțin prin cele 10 kile de cod astfel încât buba a fost oarecum imediată. Dar nu imposibil de descoperit cu puțină răbdare și stil având în vedere regulile simple de URL rewrite, destul de evidente, și faptul că era vorba de un singur parametru vulnerabil. De fapt am reușit 3 in 1: XSS, blind SQLi și disclosure în același input.

A da, a folosi mysql_error() în producție este una dintre cele mai proaste idei. XSS-ul și disclosure-ul au fost posibile prin intermediul acestei funcții magice ce ar trebui folosite doar pentru dezvoltare, nu și pentru producție.

Momentan nu dăm poze, așteptăm să aplice patch-ul trimis :).

urlencode()/rawurlencode() complet

Exceptând faptul ca Mr. Google bușește validarea blogului (deși W3 nu citește resursele externe = se validează), deasemenea exceptând faptul că WordPress mai trage câte o gherlă și refuză să pună conținut valid, este cunoscut faptul că am un oarecare fetiș cu standardele. În concluzie depun eforturi reale pentru a le respecta. Din păcate există situații rare în care caractere arbitrare din UTF-8 duc la invalidarea paginilor. Un exemplu bun este atunci când se compun link-uri ce conțin caractere non-ASCII. Soluția evidentă este URL encoding, dar din păcate funcția urlencode() din PHP, precum și rawurlencode() are boala de a omite caracterele menționate mai sus. Din moment ce dezvoltatorii PHP refuză introducerea unui flag pentru a putea face o codare completă a unui șir de caractere, există soluții ce pot fi aplicate la nivel de PHP. Soluția propusă de către subsemnatul depinde doar de componente din core (PCRE este parte a PHP core!).

function url_encode_all($text)
	{
		return preg_replace('/./e', "sprintf('%%%02X', ord('\\0'))", $text);
	}

MySQL, arbori, o singură tabelă, cheile străine si un exemplu în Kohana

Aparent am o poveste de spus. Ei bine și de data aceasta aparențele nu înșeală. M-am lovit de suficiente ori de problema arborilor stocați în baze de date, în special atunci când este vorba de o structură de categorii. Cum nu sunt un mare fan al procedurilor stocate și al trigger-elor, sunt de părere că impunând o constrângere de cheie străina problema se rezolvă mult mai elegant atunci când vine vorba să se șteargă toată ierarhia. MyISAM nu știe el de chei străine deci aici vine în ajutor InnoDB. InnoDB știe de chei străine, dar cel mai probabil prin transformarea tabelei problema arborelui nu se rezolvă de la sine deoarece nu se pot impune constrângerile de cheie străina.

Ideea este următoarea: pentru a impune o cheie străina folosind MySQL și InnoDB este nevoie ca structura arborelui să fie corecta și aceasta să convină lui InnoDB. A doua parte este partea spinoasă. Cel mai probabil un arbore corect definit are un lucru esențial ce îi lipsește. Se dă următoarea structură de bază:

CREATE TABLE `trees` (
`id` INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`parent_id` INT( 11 ) UNSIGNED NOT NULL DEFAULT '1',
`data` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL ,
INDEX ( `parent_id` )
) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci

Precum se observă schema de date este corectă iar aparent nu pot apărea probleme. Se dă chiar următorul arbore:

1, 0, a
2, 0, b
3, 1, c
4, 1, d
5, 3, e
6, 2, f

Deși (aparent) la prima vedere structural este corect, dacă se rulează interogarea ce ar trebui sa pună constrângerea:

ALTER TABLE `trees` ADD FOREIGN KEY ( `parent_id` ) REFERENCES `trees` (
`id`
) ON DELETE CASCADE ;

o să dea o eroare de MySQL de toată frumusețea, ceva gen:

#1452 - Cannot add or update a child row: a foreign key constraint fails
([...]`, CONSTRAINT `[...]` FOREIGN KEY (`parent_id`)
REFERENCES `trees` (`id`) ON DELETE CASCADE)

Cei cu privirea mai ageră poate că s-au prins de soluție. Nu întâmplător i-am dat valoare implicită 1 lui parent_id, ba chiar mai mult, exemplul dat de mine nu este UN arbore, ci mai mulți arbori stocați în aceeași tabelă. Am dat acest exemplu pentru că am întâlnit de suficiente ori în practică asemenea implementări ce mai încolo îmi dădeau bătăi de cap. Ba chiar am moștenit o astfel de baza de date pentru un proiect. Ceea ce confirmă zicala care spune faptul că web developerii sug la SQL – iar cum sunt și eu în breasla lor mă auto-includ. Dar din când în când, mai există și momente de deșteptare.

Soluția sună cam asa: se face backup la date. Cheile primare nu se salvează, coloana parent_id ce va deveni cheie străina se incrementează cu o unitate dacă este vorba de o implementare precum cea din exemplul meu, dacă nu, atunci se adaptează. Se golește tabela. Se aplică iarăși codul de mai sus pentru crearea cheii străine (ALTER TABLE bla, bla, bla). În mod deloc surprinzător, aceasta va funcționa fără probleme. Nu va mai da eroarea #1452. Dar, pentru a continua, este imperativ ca această înregistrare să se găsească în tabelă:

INSERT INTO `trees` (
`id` ,
`parent_id` ,
`data`
)
VALUES (
'1', '1', 'root'
);

‘root’ nu e musai să fie ‘root’. Poate fi și ‘rădăcină’, dar cum personal scriu mai rar soft pentru România, prefer denumirile în Engleză. Aceasta trebuie să fie rădăcina arborelui. O înregistrare de genul (0, 0, ‘root’) este invalidă, deci nu va putea fi folosită pentru a valida implementarea originală. De aici se pot insera mai departe înregistrările, fără bătăi de cap atâta timp cât se păstrează integritatea relației cheie primară – cheie străină. De altfel, această înregistrare NU trebuie ștearsă. Dacă se șterge, se va șterge automat tot arborele. Este evident … dacă tai un copac de la rădăcina, cade cu totul. Dacă tai doar o creangă, restul copacului nu este afectat. În concluzie, dacă datele sunt manipulate corect, nu pot apărea probleme. Mai departe se poate insera noul arbore:

INSERT INTO `trees` (
`parent_id` ,
`data`
)
VALUES (
1, 'a'
), (
1, 'b'
), (
2, 'c'
), (
2, 'd',
), (
4, 'e'
), (
3, 'f'
);

Se va obține rezultatul dorit inițial. Iar constrângerea își face magia: spre exemplu dacă se șterge (2, 1, ‘a’), atunci automat se vor șterge și (4, 2, ‘c’), (5, 2, ‘d’) și (6, 4, ‘e’) pentru că ierarhic au ca părinte pe (2, 1, ‘a’).

Pentru că tot m-a prins microbul MVC, mai bine zis Kohana, o să dau ca exemplu un model ce tratează din punctul de vedere al aplicației problema de mai sus. Din moment ce în mod implicit Kohana este destul de restrictiv cu manipularea variabilelor, altfel spus chestii ce în mod normal PHP le tratează ca ‘Notice’ în Kohana pot da in ‘Runtime Error’, metodele sunt puțin mai stufoase în sensul că verifică integritatea înainte de a acționa. Exemple asupra a ceea ce am spus mai sus: nu se poate șterge sau actualiza o înregistrare ce este deja ștearsă. În mod normal aceste operații returnează 0, fie ca e vorba de ‘affected rows’ sau de ‘deleted rows’. Cel puțin așa se întâmplă în mod implicit în ambele situații descrise, nu am cercetat dacă este configurabil acest comportament și nici nu am de gând. Kohana prin strictețe impune modele sănătoase de programare ce nu și le însușește orice cocalar ce pune mâna pe PHP pentru faptul că variabilele încep cu $ și a auzit că poate să se simtă și el h4x0r pentru că poate programa ceva. Deci să îi dau bâte cu modelul:

class Tree_Model extends Model {
 
	public function __construct($id = NULL)
	{
		parent::__construct($id);
	}
 
	public function add_child($parent_id, $data)
	{
		$check_record = $this->db->where('id', $parent_id)->get('trees');
 
		if($check_record->count() > 0)
		{
			return $this->db->from('trees')->set(array('parent_id' => $parent_id, 'data' => $data))->insert();
		}
		else
		{
			return false;
		}
	}
 
	public function update_child($id, $parent_id, $data)
	{
		$check_record1 = $this->db->where('id', $id)->get('trees');
		$check_record2 = $this->db->where('id', $parent_id)->get('trees');
 
		if($check_record1->count() === 1 AND $check_record2->count() > 0)
		{
			return $this->db->from('trees')->set(array('parent_id' => $parent_id, 'data' => $data))->where('id', $id)->update();
		}
		else
		{
			return false;
		}
	}
 
	public function delete_child($id)
	{
		$check_record = $this->db->where('id', $id)->get('trees');
 
		if($check_record->count() === 1 AND $id > 1)
		{
			return $this->db->from('trees')->where('id', $id)->delete();
		}
		else
		{
			return false;
		}
	}
 
}

PS: clasa este portabilă și pentru alte baze de date având în vedere faptul că am folosit ‘Database Query Builder’. Mergea si cu ORM, dar prefer o abordare puțin mai directa a problemei. În ORM nu mă mai simt stăpân pe situație. Pentru cârcotași: având în vedere că aceste tipuri de structuri de date nu prea sunt dedicate modificărilor dese, impactul de performanță datorat numărului mărit de interogări pentru a verifică integritatea este minim și merită efortul pentru a face o abordare corectă în loc să se apeleze la reguli mai puțin stricte și a depinde de erorile eventuale returnate de către baza de date (FK constraint error).

Nota: nu pot corecta comportamentul cretin al WordPress pentru a afisa corect apostrof si ghilimele.

Instalarea Apache 2.2 + PHP 5.2 + MySQL 5.1 sub Windows

Ultima actualizare: 19 Aprilie 2009

Introducere

Desi exista o droaie de pachete de astea ce le contin pe toate si au o gramada de arome, pe zi ce trece ajung la concluzia ca un developer serios nu se incurca cu mizerii si isi seteaza singur mediul de dezvoltare a aplicatiilor web. Ma rog, nu toate sunt mizerii, dar majoritatea fie sunt prea stufoase, fie inutile sau inutilizabile fara lucru manual – deci mai bine faci lucru manual din start. Asta in cazul in care nu vrei sa ramai o mimoza pentru tot restul vietii care transpira cand vine vorba sa adauge module noi mediului respectiv, sau pur si simplu sa se blocheze in parametrii de configurare relativ simpli. Nu sustin faptul ca este usor. De altfel patrunderea in tainele configurarii fine necesita timp si multa documentatie citita. Dar macar vreo cateva chestii de baza ar trebui cunoscute. Iar chestiile de baza pornesc cu instalarea.

Propozitie cheie: daca iti este lene sa iti configurezi acest mediu (lucru ce nu se intampla zilnic, iar experienta acumulata este benefica) atunci oare nu iti este lene sa te apuci sa programezi catusi de cat mai mult decat aplicatii gen “hello world”? Pentru a implementa solutii de o complexitate relativ mare ce necesita varii module este nevoie de mult mai multa munca pentru documentare decat pentru a configura un mediu de dezvoltare. In plus, din moment ce nu iti cunosti bine mediul in care ruleaza aplicatiile tale, cum poti avea nesimtirea sa sustii ca ai idee foarte bine ce face propria aplicatie? De unde vei sti ca va functiona corect si este portabila pe alta masina? Intrebarile acestea sunt multe, si nu, nu am de gand sa le transform in intrebari retorice. Raspunsurile sunt mai mult sau mai putin evidente.

O sa structurez acest articol in cativa pasi destul de simplu de urmat. Este ca in cazul in care se construieste o casa: se pleaca de la fundatie, si se termina treaba cu acoperisul. Deci ordinea va respecta logica si bunul simt, cu notiunea ca desi o sa incep cu Apache, MySQL deasemenea poate fi primul pas deoarece baza de date si serverul web nu sunt interdependente. PHP se integreaza cu Apache, deci regulile anterior mentionate indica faptul ca va fi instalat dupa el – aceasta pentru a nu fi nevoit sa faci instalarea PHP de doua ori. Continue reading