David Steinsland – informatikkstudent og webutvikler

Innlegg merket med «php»

Datoer i PHP – på den enkle måten

| Ingen kommentarer »

Å jobbe med datoer kan være riktig slitsomt til tider. Hvor mange ganger har du ikke klødd deg litt i bakhodet, og tenkt hvordan i huleste du kan formatere datoer som enten er formatert forskjellig, er fra ulike tidssoner eller som du må trekke fra / legge til dager/uker/måneder … ?

Ja, det har vært et problem. Med det mener jeg at løsningen er på plass, og at du kan slappe ekstra godt av. Dette innebærer selvfølgelig at du har en PHP-installasjon tilsvarende 5.3.x.

Alle dager i mars måned

Tidligere har vi måttet brukt PHPs datofunksjoner (date/strtotime/strftime …), men siden PHP 5.3 kan vi nemlig dra nytte av de nye DateTime-klassene og godene som medfølger.
Se bare hvor enkelt det er å skrive ut alle dagene i Mars 2011:

$march = new DateTime ('March 2011');

$days = new DatePeriod (
    $march,
    new DateInterval ('P1D'),
    $march->modify ('first day of next month')
);

foreach ($days as $day)
{
    echo $day->format ('Y-m-d'), "\n";
}

Årsaken til at jeg valgte å skrive «first day of next month» fremfor «last day of» er på grunn av at siste datoen blir ekskludert, som hadde betydd at 31. mars ikke hadde blitt skrevet ut.

I eksempelet mitt har jeg benyttet meg av alle klassene som inngår i DateTime-biblioteket (med unntak av DateTimeZone) hvor:

  • DateTime lager et objekt med 1. mars 2011 utifra «March 2011″
  • DatePeriod lager et objekt som inneholder alle datoer fra 1. mars til 1. april (eksklusivt) hvor det er 1 dag mellom hver instans (dette bestemmes av DateInterval)

Antall dager mellom to datoer

DateTime-klassen har et stort bruksområde, og kan for eksempel brukes til å finne differansen mellom to datoer:


$january = new Datetime ('January 2011');

$diff = $january->diff ( new DateTime ('April 2011') );

echo 'Difference between January and April: ',
$diff->format ('%R%a days');  // 90 days

%a fungerer slik at den skal gi meg det totale antallet dager mellom de to datoene. Denne funksjonaliteten fungerer dessverre ikke optimalt på Windows, og kan skape endel frustrasjoner. Men i lag med PHP 5.3 kan du også sammenligne to DateTime-instanser ved bruk av «comparison operators» (større/mindre enn, osv.) Dette gjør oss istand til å lage en «work-around» til Windows-problematikken:


$january = new DateTime ('January 2011');
$april = new DateTime ('April 2011');

// clone variable to keep $january clean..
$currdate = clone $january;

for ($days = 0; $currdate->modify ('+1 day') <= $april; ++$days);

echo "Difference between January and April: {$days} days";

Utvide DateTime med støtte for MySQL Datetime

Det er også ganske tilfredsstillende å benytte seg av DateTime ilag med MySQL DATETIME. I dette eksempelet har jeg valgt å utvide DateTime-klassen:

class MyDateTime extends DateTime
{
    const MYSQL = 'Y-m-d H:i:s';

    public static function createFromMySQL ($datetime)
    {
        return self::createFromFormat (self::MYSQL, $datetime);
    }
}

// date from MySQL
$date = MyDateTime::createFromMySQL ($row['my_date']);

$timestamp = $date->getTimestamp ();

echo "Date: {$date->format ('d.m.Y H:i')}\n";

Poenget mitt var ikke å utvide klassen for å legge til en enkel funksjon, men heller illustrere at det kan være lurt å gjøre det (DateTime-klassen har tross alt ikke tatt høyde for absolutt alt).
Kanskje det kunne vært en idé å gjøre Windows work-arounden min til en funksjon som utvider DateInterval ?

Som regel er det kun DateTime du har behov for i det daglige, men du kan også få god nytte av både DateInterval og DatePeriod om du for eksempel skal jobbe med kalendere.

Cache PHP-sider: kort og enkelt

| Ingen kommentarer »

Når man skriver nettsider med PHP er det mye som kan være med på å dra opp lastetiden. For ikke å snakke om alle CSS- og JavaScript-filer som også må lastes ned. Hva kan man så gjøre?

Om du jobber med databaser kan du for eksempel skru på MySQL Query Cacher, samtidig som du kan mellomlagre resultatet i HTML-, JSON eller XML-format. Men alt dette krever igrunn litt arbeid, samt at noen koder her og der må endres.

$lastModified = filemtime (__FILE__);
$etagFile = md5_file (__FILE__);
$ifModifiedSince = isset ($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : FALSE;
$etagHeader = isset ($_SERVER['HTTP_IF_NONE_MATCH'] ? trim ($_SERVER['HTTP_IF_NONE_MATCH']) : FALSE;

header("Last-Modified: " . gmdate("D, d M Y H:i:s", $lastModified) . " GMT");
header("Etag: $etagFile");
header('Cache-Control: public');

//check if page has changed. If not, send 304 and exit
if (@strtotime ($ifModifiedSince) == $lastModified || $etagHeader == $etagFile)
{
       header ("HTTP/1.1 304 Not Modified");
       exit;
}

//your normal code below

Om du limer inn koden ovenfor i starten på de PHP-filene du ønsker å mellomlagre, merker du forskjellen med én gang. Jeg har selv testet koden i flere prosjekter, og den fungerer utmerket. Det som er verdt å merke seg, er at den ikke fanger opp endringer i dynamisk innhold med én gang.
Den merker så klart endringer på seg selv, men dersom du henter innhold fra en database så kan det ta noen minutter før det vises.

Denne prosessen kan, så vidt jeg vet, ikke fremskyndes siden mellomlageret ligger i nettleseren, og det er ikke mulig å fjerne det (dette må sluttbruker gjøre manuelt).

RecursiveIterator med egne filtre

| Ingen kommentarer »

Er det noe jeg virkelig elsker med PHP, må det være Standard PHP Library, eller SPL som det også heter. Det er en samling av innebygde klasser som gjør deg istand til for eksempel å kjøre (iterere) gjennom arrays, filer, mappestrukturer (endimensjonalt) eller flerdimensjonalt (rekursiv). Alt dette ved en solid OOP-struktur!

Scenario: du skal iterere rekursivt gjennom en mappestruktur, og ønsker kun å hente ut filer med endelsen «txt».

Hva gjør du? Du kunne brukt en kombinasjon av scandir() og din egen rekursive funksjon, eller glob(), eller, eller…

Hva dersom du ønsker å bruke kodene senere, flere ganger, bare med små endringer? Det er virkelig en grense for hvor fleksibelt et system kan være når det er satt til å utføre én bestemt oppgave. Utnytter du OOP, kan du snu på dette.

I koden nedenfor henter jeg ut alle filer (uansett nivå) som befinner seg inni mappen filer/.

foreach (new RecursiveIteratorIterator(
	new RecursiveDirectoryIterator('filer')
) as $file
)
{
    echo $file->getFilename();
}

Men skulle ikke jeg filtrere bort enkelte filer? Hvordan gjør jeg det?

Det vi trenger å gjøre, er å lage en klasse i PHP som arver egenskaper fra FilterIterator. Den må fyre i gang RecursiveDirectoryIterator og RecursiveIteratorIterator, i tillegg må den ha metoden accept().

Her har jeg skrevet enda et enkelt eksempel, hvor jeg kun henter ut filer med endelsen «txt».

class RecursiveFilter extends FilterIterator
{
	protected $_extensions = array ();

	public function __construct ($path, array $extension = array ())
	{
		$this->_extensions = $extensions;

		parent::__construct(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)));
	}

	/**
	 * Bestemmer hvilke filer som skal bli godtatt
	 * Metoden må returnere FALSE eller TRUE, alt ettersom
	 * om filen er godtatt eller ei.
	 */
	public function accept ()
	{

		// get the file that are being iterated
		$item = $this->getInnerIterator ();

		return in_array ( pathinfo ($item->getBasename(), PATHINFO_EXTENSION), $this->_extensions);
	}
}

$iterator = new RecursiveFilter ('filer', array ('txt'));

foreach ($iterator as $item)
{
	echo $item->getBasename();
}

Så enkelt kan det gjøres. I teorien har vi igrunn fjernet sjekken ut av foreach-løkken, og inn i egen klasse. Det virker unødvendig, men med tanke på gjenbruk og abstraksjon så er dette en meget god idé.

Om du ønsker alle filer som ikke har endelsen «txt», så snur du bare på sjekken som utføres i accept() til:

return ! in_array ( pathinfo ($item->getBasename(), PATHINFO_EXTENSION), $this->_extensions);

Du kan selvfølgelig utvide accept() til så mye du vil, og ta høyde for alt du ønsker. $item inneholder et FileInfo-objekt, så da kan du bare slå deg løs!

Daglig backup av MySQL med PHP (Windows)

| Ingen kommentarer »

Dersom du drifter din egen server eller har en utviklingsserver på PC-en din, har du nok vært i situasjoner hvor du tenker: «Hvor er sikkerhetskopien når jeg trenger den?».

Jeg var der selv for omtrent en úke siden, hvor jeg ved en feiltakelse kjørte en gammel PHP-fil som overskrev hele Postnummer-databasen min. Heldigvis hadde jeg mange sikkerhetskopier av databasen, så det gikk bra.

Men hva om jeg ikke hadde hatt kopier? Da hadde faktisk hele arbeidet mitt vært ødelagt! Mangfoldige timer bortkastet, bokstavelig talt.

Hver dag (midnatt) blir livredderen utført: en planlagt oppgave i Windows kjører et PHP-skript som tar backup av MySQL.

I mine siste artikler har jeg skrevet litt om planlagte oppgaver i Windows, og vist flere ting du kan gjøre.

Teori

Når du installerte MySQL, fikk du med et program som heter mysqldump. Programmet brukes til å ta backup av MySQL; du kan selv spesifisere én eller flere databaser, eller om du ønsker å ta backup av alle.

Videre kommer jeg til å ta i bruk  PHP fra kommandolinjen, som muliggjør at vi kan bruke PHP-tolkeren til å kjøre en bestemt PHP-fil. Dette programmet ligger i installasjonsmappen til PHP, og heter php.exe

Jeg refererer til disse programmene som henholdsvis mysqldump og php fra kommandolinjen. Dette er fordi jeg har lagt til PHPs installasjonsmappe og bin-mappen til MySQL til miljøvariabelen PATH. Dette forteller Windows at når du skriver inn et program i CMD, skal den også leite etter det programmet i de plasseringene.

Dersom du ikke har gjort dette, må du spesifisere til programmene med full sti, eks: c:\php\php eller skrive inn følgende i CMD:

SET PATH = %PATH%;C:\PHP;C:\MySQL\bin

(jeg antar PHP og MySQL er installert i C:\).

Ved default ligger «.EXE» i miljøvariabelen PATHEXT, som medfører at du slipper å skrive «.exe» bak programnavnet.

PHP-filen

Gjør klar en PHP-fil som du navngir backup.php og plasser denne utenfor ServerRoot (altså en plass den ikke kan nåes via en nettleser).

ServerRoot hos meg er \www\public_html\, slik at jeg har plassert PHP-filen ett steg opp, i \www\ (Med baklengs-skråstrek foran stien, tolker Windows dette som C:\).

Lag så en mappe du kaller for backup i samme mappe hvor du har plassert PHP-filen. Dette er plassen hvor backupene våre kommer til å ligge.

Det første vi gjør i PHP-filen vår er å definere et par runtime-innstillinger, som databasetilkoblingen, plasseringen til backup-filene og hvordan filnavnet skal utformes.

// MySQL host
define ('HOST', 'localhost');
// MySQL username
define ('USER', 'username');
// MySQL password
define ('PW', 'password');

define ('DUMP_FILENAME', date ('Y-m-d-H-i') . '.sql'); // i.e. 2010-10-30-23-00.sql

define ('SELF_DIR', __DIR__ . DIRECTORY_SEPARATOR);
define ('BACKUP_DIR', 'backup' . DIRECTORY_SEPARATOR);

Backupfilene våre får navnet sitt basert på dagens dato og tidspunktet for når filen blir kjørt.

Neste steg er å kalle opp mysqldump:

passthru (sprintf ('mysqldump -h %s -u %s -p%s -A -r "%s"', HOST, USER, PW, SELF_DIR . BACKUP_DIR . DUMP_FILENAME));

Her trenger du ikke endre noe, bortsett fra stien til mysqldump om du ikke har endret PATH i Windows.

Argumentforklaring:

-h
Spesifiserer host-adressen til MySQL
-u
Brukernavnet til MySQL
-p
Passordet ditt MySQL-brukeren (-p og passordet skal ikke ha mellomrom mellom hverandre).
-A
Spesifiserer at vi skal ta backup av alle databasene. Alternativt –all-databases.
-r
Den fulle stien (plassering + filnavn) til hvor backupen ligger

Det neste jeg har valgt å gjøre, er å komprimere SQL-filen i et Zip-arkiv, som medfører at filstørrelsen blir endel mindre (noe som er bra!).

$Zip = new ZipArchive;
$Zip->open (SELF_DIR . BACKUP_DIR . DUMP_FILENAME . '.zip', ZipArchive::CREATE);
$Zip->setArchiveComment ('This database dump was automatically taken at ' . date ('H:i, d.m.Y') . ' by a Windows Scheduled Task');
$Zip->addFile (DUMP_FILENAME, SELF_DIR . BACKUP_DIR . DUMP_FILENAME);
unlink (SELF_DIR . BACKUP_DIR . DUMP_FILENAME);
$Zip->close();

Den planlagte oppgaven

Det siste steget vårt, er å opprette oppaven som skal kjøre PHP-skriptet. Jeg har valgt å kjøre denne oppgaven hver dag ved midnatt.

Opprett den planlagte oppgaven ved å skrive dette inn i CMD:

SCHTASKS /Create /SC DAILY /MO 1 /ST 00:00 /TN «MySQL Backup» /TR «php \www\backup.php»

Tips

Uten PHP

Dersom du ikke ønsker å komprimere filene med Zip, trenger du heller ikke anvende PHP til denne jobben. Da kjører du rett og slett bare mysqldump direkte i oppgaven:

SCHTASKS /Create /SC DAILY /MO 1 /ST 00:00 /TN «MySQL Backup» /TR «mysqldump -h <host> -u <brukernavn> -p<passord> -A -r \www\backups\database_dump.sql»

Husk at det ikke skal være mellomrom mellom -p og passordet!

Én eller flere databaser istedenfor alle

Erstatt -A i mysqldump-kommandoen med:

–database <database>

eller

–databases <database 1> <database 2> … <database n>

Garbage Collector

Det tar ikke lange tiden før backup-mappen blir full av filer; og strengt talt så trenger du ikke backups som ble tatt for måneder siden. Derfor kan det være lurt å sette opp en søppelsamler, som sletter gamle filer.
Da oppretter du en planlagt oppgave som kjører for eksempel \www\garbage.php hver uke. Innholdet i PHP-filen er å loope gjennom backup-mappen, og sjekke datoen på når filene sist ble modifisert (opprettet). Slik har jeg gjort det:

// How long we are keeping each file, since its creation date
// (in seconds)
define ('MAX_FILE_AGE', (60 * 60 * 24 * 7)); // 7 days

$data = glob (__DIR__ . DIRECTORY_SEPARATOR . 'backup' . DIRECTORY_SEPARATOR . '*.zip');

foreach ($data as $file)
{
	if (time() - filectime ($file) > MAX_FILE_AGE)
	{
		unlink ($file);
	}
}

Sikkerhet i PHP gjort lettere

| 5 kommentarer »

Når du utvikler en nettside, enten det er en applikasjon, rammeverk, bibliotek — hva som helst — er det desidert viktigste sikkerhet. Likevel er det mange som tar snarveier, og slurver seg unna. Om du vil unngå å stå med skjegget i postkassen den dagen du blir angrepet via XSS, CSRF eller SQL injections bør du følge med nå.

Hvorfor?

Du skal aldri stole på brukerne dine. Selv om det er familien eller vennene dine. Årsaken er at enkelte personer kan utnytte sårbarhetene i systemet ditt, som kan resultere i at sensitiv informasjon blir samlet, slettet eller endret. Det kan resultere i at noen tar fysisk kontroll over nettsiden din.

Hva kan man gjøre?

Ved å følge prinsippet ovenfor, må du sørge for å validere all data du skal hente inn fra brukerne. Du bør også sanitere data.

Validering: sjekke om data er av riktig type, for eksempel en ordentlig E-postadresse eller postnummer.
Sanitering: manipulasjon av data, som for eksempel å fjerne enkelte tegn/bokstaver/tall og så videre.

I PHP får vi tilgang til brukerdata via super-globalene $_GET, $_POST, $_COOKIE, $_SERVER og $_SESSION.

Her kan brukere bokstavelig talt skrive hva de vil, og det kan betyr store følger om du ikke sjekker dataene ordentlig.

Løsningen

Jeg har skrevet et bibliotek i PHP kalt «PHP Security Wrapper«. Dette biblioteket tar seg av brukerdata fra GET, POST, SERVER og COOKIE, samtidig som du får muligheten til både å validere og sanitere dem.

Biblioteket er selvfølgelig gratis, og oppdateres hyppig. Om du ønsker å lære mer om dette biblioteket kan du lese forumtråden jeg postet på Norsk Webforum, sjekke ut nettsiden og lese dokumentasjonen.

Det er ikke meningen at dette blogginnlegget skulle ta for seg hvordan du kan sikre deg mot angrep, men om du ønsker å lese videre på emnet kan du sjekke ut følgende linker: