Riepilogo post nella categoria Programmazione PHP

Nota: questa guida creata da Maurizio Fonte è anche disponibile come GIST su GitHub all'indirizzo https://gist.github.com/mauriziofonte/00002661cd527a8a457509c5b4b0613d.

Lo sviluppo web con tecnologie puramente Linux (Stack LAMP - Linux, Apache, Mysql, PHP) è diventato estremamente semplice su Windows 10.

Infatti, con WSL è possibile, citando la pagina ufficiale di Microsoft: Sottosistema Windows per Linux consente agli sviluppatori di eseguire un ambiente GNU/Linux, inclusi la maggior parte degli strumenti da riga di comando, delle utilità e delle applicazioni, direttamente in Windows, senza modifiche e senza il sovraccarico di una macchina virtuale tradizionale o di una configurazione di avvio doppio.

Inoltre, sempre Microsoft è riuscita a creare uno degli editor di codice più potenti e completi in circolazione: Visual Studio Code. Una volta iniziato ad utilizzare, sarà molto molto difficile tornare indietro.

E' importante quindi conoscere come configurare un ambiente di sviluppo confortevole, che permetta di dire addio al vecchio modus operandi inefficiente e macchinoso che obbligava l'uso di WinSCP e Notepad++ con modifiche pubblicate direttamente sui server di produzione (o altri sistemi similari).

Con lo stack LAMP su Windows e l'utilizzo di Visual Studio Code e la continuous integration di Git, sarà possibile essere più produttivi e testare le condizioni reali di produzione direttamente sul proprio PC.

Nel corso del tempo, ho creato una guida esaustiva che spiega come configurare un ambiente di sviluppo comodo con stack LAMP utilizzando WSL2 su Windows. E' una lettura lunga, si consiglia di leggerla con calma, cercando di comprendere ogni passaggio, ed eventualmente adattarla ai propri scopi. Questa guida è anche disponibile a questo indirizzo, per una più comoda lettura: https://gist.github.com/mauriziofonte/00002661cd527a8a457509c5b4b0613d

Importante: come precondizione per un buon uso di questa guida, è necessaria una conoscenza minima del terminale di Linux e una buona dimestichezza con Windows.

Attenzione: questa guida è stata scritta per Windows 10. Tuttavia, potrebbe funzionare su Windows 11, ma non ho avuto modo di testarla dal vivo. Su Windows 11, quindi, seguire una guida specifica per l'abilitazione di WSL2, e tralasciare la parte WSL2 presente in questa guida.

Stack di sviluppo LAMP su Windows 10 con WSL2 e Visual Studio Code

Questa guida illustrerà come installare il supporto al sottosistema Linux nativo di Windows (WSL2), creare uno stack LAMP al suo interno, e agganciare Visual Studio Code per lo sviluppo direttamente su filesystem Linux.

La guida è aggiornata ad Agosto 2022 e necessita di questi prerequisiti:

  1. Computer con Windows 10, aggiornato all'ultima release disponibile
  2. 16GB di RAM
  3. 30GB di spazio libero su C:\ (conterrà il disco virtuale di Ubuntu)
  4. Un SSD (meglio NVMe) come disco di avvio di Windows (migliori performance con Mysql)
  5. Una conoscenza di base del terminale Linux (cd, cp, mv, sudo, nano, etc.)

Lo stack LAMP che andremo a configurare supporta https (con certificati autofirmati con scadenza a 10 anni), protocollo http/2 e compressione brotli. Per quanto riguarda la parte PHP, useremo PHP-FPM perchè è più performante e più versatile nella configurazione delle impostazioni per-virtualhost. Per capire le differenze tra l'utilizzo di PHP con Apache in modalità PHP-CGI piuttosto che PHP-FPM, si rimanda a questa guida: https://www.basezap.com/difference-php-cgi-php-fpm/

Installare Ubuntu 20.04 LTS su Windows

  1. Leggere il warning sotto (La versione di WSL deve essere la 2).
  2. Abilitare "Sottosistema Linux" da "Programmi e funzionalità"
  3. Cercare "Ubuntu" nel Windows Store e installare UBUNTU 20.04 LTS
  4. Avviare la macchina, attendere l'installazione, scegliere un nome utente (occhio, serve sotto ed è importante) composto da una singola parola corta tutta in minuscolo + la relativa password (corta, 1 sola lettera per comodità)
  5. Creare un collegamento desktop alla macchina Linux
  6. Creare un collegamento desktop alla directory home dell'utente, esempio: \\wsl$\Ubuntu-20.04\home\maurizio

WARNING: la versione di WSL DEVE essere la 2

Questo è molto importante. Se non è installato WSL2, non funziona nulla.

Per questo motivo, leggere https://aka.ms/wsl2-install. SI CONSIGLIA DI FARE QUESTA OPERAZIONE PRIMA DI INSTALLARE UBUNTU SUL SISTEMA.

Se necessario abilitare WSL2 (perchè non già abilitato), bisognerà aprire una PowerShell con privilegi elevati e usare questi comandi:

Installare questo pacchetto: https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi

dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
Restart-Computer                    # bisogna riavviare
wsl --set-version Ubuntu-20.04 2    # IMPORTANTE: questo è necessario solo se si è installato ubuntu prima di abilitare WSL2
wsl --set-default-version 2

Modificare il resolver DNS di WSL2

Guida copiata da: https://superuser.com/questions/1533291/how-do-i-change-the-dns-settings-for-wsl2

A) Turn off generation of /etc/resolv.conf

Using your Linux prompt, (I'm using Ubuntu), modify (or create) /etc/wsl.conf with the following content

[network]
generateResolvConf = false

(Apparently there's a bug in the current release where any trailing whitespace on these lines will trip things up.)

B) Restart the WSL2 Virtual Machine

Exit all of your Linux prompts and run the following Powershell command

wsl --shutdown

C) Create a custom /etc/resolv.conf

Open a new Linux prompt and cd to /etc

If resolv.conf is soft linked to another file, remove the link with rm resolv.conf (originally it is linked to ../run/resolvconf/resolv.conf)

Create a new resolv.conf with the following content

nameserver 1.1.1.1

Configurare l'ambiente LAMP su Ubuntu - Step 1

Qui andremo ad installare tutti i pacchetti di PHP in tutte le versioni dalla 5.6 alla 8.1, nonchè il web server Apache e il server Mysql.

Perchè installare tante versioni di PHP? Perchè è importante avere a disposizione un ambiente di sviluppo che consenta, con facilità, di testare la propria applicazione con svariate versioni di PHP. Questo agevolerà il lavoro in caso di constraint specifici sui server di produzione dove andremo ad installare le applicazioni create.

Si presume che la versione di default di PHP che si vorrà utilizzare nel sistema sia la 8.1. Questo è modificabile tramite le righe update-alternatives --set php*** che si troveranno nella lista qui sotto. Ad esempio, se si desidera che la versione di PHP di default (quella che verrà utilizzata digitando semplicemente il comando php e non la sua versione "versionata" es php7.4) basterà specificare update-alternatives --set php /usr/bin/php7.4.

IMPORTANTE: Lanciare tutti questi comando come l'utente root. IMPORTANTE: Escludere le linee che iniziano con ### in quanto servono solo a differenziare i vari blocchi.

# APT UPGRADE & INSTALL OF PHP + APACHE
apt update && apt upgrade
apt install net-tools unzip git lsb-release ca-certificates apt-transport-https software-properties-common -y
add-apt-repository ppa:ondrej/php
add-apt-repository ppa:ondrej/apache2
apt install openssl apache2 brotli libapache2-mod-fcgid libapache2-mod-php8.1 php8.1 php8.1-cli php8.1-fpm php8.1-common php8.1-bcmath php8.1-bz2 php8.1-curl php8.1-gd php8.1-intl php8.1-mbstring php8.1-mcrypt php8.1-mysql php8.1-xml php8.1-zip libapache2-mod-php8.0 php8.0 php8.0-cli php8.0-fpm php8.0-common php8.0-bcmath php8.0-bz2 php8.0-curl php8.0-gd php8.0-intl php8.0-mbstring php8.0-mcrypt php8.0-mysql php8.0-xml php8.0-zip libapache2-mod-php7.4 php7.4 php7.4-cli php7.4-fpm php7.4-common php7.4-bcmath php7.4-bz2 php7.4-curl php7.4-gd php7.4-intl php7.4-json php7.4-mbstring php7.4-mcrypt php7.4-mysql php7.4-xml php7.4-zip libapache2-mod-php7.3 php7.3 php7.3-cli php7.3-fpm php7.3-common php7.3-bcmath php7.3-bz2 php7.3-curl php7.3-gd php7.3-intl php7.3-json php7.3-mbstring php7.3-mcrypt php7.3-mysql php7.3-xml php7.3-zip libapache2-mod-php7.2 php7.2 php7.2-cli php7.2-fpm php7.2-common php7.2-bcmath php7.2-bz2 php7.2-curl php7.2-gd php7.2-intl php7.2-json php7.2-mbstring php7.2-mcrypt php7.2-mysql php7.2-xml php7.2-zip libapache2-mod-php7.1 php7.1 php7.1-cli php7.1-fpm php7.1-common php7.1-bcmath php7.1-bz2 php7.1-curl php7.1-gd php7.1-intl php7.1-json php7.1-mbstring php7.1-mcrypt php7.1-mysql php7.1-xml php7.1-zip libapache2-mod-php7.0 php7.0 php7.0-cli php7.0-fpm php7.0-common php7.0-bcmath php7.0-bz2 php7.0-curl php7.0-gd php7.0-intl php7.0-json php7.0-mbstring php7.0-mcrypt php7.0-mysql php7.0-xml php7.0-zip libapache2-mod-php5.6 php5.6 php5.6-cli php5.6-fpm php5.6-common php5.6-bcmath php5.6-bz2 php5.6-curl php5.6-gd php5.6-intl php5.6-json php5.6-mbstring php5.6-mcrypt php5.6-mysql php5.6-xml php5.6-zip
update-alternatives --set php /usr/bin/php8.1
update-alternatives --set phar /usr/bin/phar8.1
update-alternatives --set phar.phar /usr/bin/phar.phar8.1
update-alternatives --set phpize /usr/bin/phpize8.1
update-alternatives --set php-config /usr/bin/php-config8.1
### OPTIONAL SQLITE3 SUPPORT
apt install php5.6-sqlite3 php7.0-sqlite3 php7.1-sqlite3 php7.2-sqlite3 php7.3-sqlite3 php7.4-sqlite3 php8.0-sqlite3 php8.1-sqlite3
### OPTIONAL REDIS SUPPORT
sudo apt install redis-server php5.6-redis php7.0-redis php7.1-redis php7.2-redis php7.3-redis php7.4-redis php8.0-redis php8.1-redis
# APACHE HTTP2 + PHP-FPM + SSL + REWRITE + BROTLI
a2dismod php5.6 php7.0 php7.1 php7.2 php7.3 php7.4 php8.0 php8.1 mpm_prefork
a2enconf php5.6-fpm php7.0-fpm php7.1-fpm php7.2-fpm php7.3-fpm php7.4-fpm php8.0-fpm php8.1-fpm
a2enmod actions fcgid alias proxy_fcgi rewrite ssl http2 mpm_event brotli
# INSTALL MYSQL
apt install mysql-client-8.0 mysql-server-8.0

Configurare l'ambiente LAMP su Ubuntu - Step 2

Qui andremo a modificare le configurazioni di base di Apache e Mysql per poter lavorare localmente.

  1. Aprire il file /etc/apache2/envvars con nano e modificare APACHE_RUN_USER e APACHE_RUN_GROUP settandoli, al posto che www-data, con il proprio nome utente
  2. Aprire il file /etc/apache2/ports.conf con nano e modificare ogni occorrenza di Listen con Listen 127.0.0.1 (esempio: 127.0.0.1:80 127.0.0.1:443)
  3. Lanciare service apache2 restart per applicare le modifiche alla config di Apache.
  4. Aprire il file /etc/mysql/mysql.conf.d/mysqld.cnf e aggiungere queste righe (sostituisci [LF] con delle andate a capo) nella sezione [mysqld], (per intenderci, sotto la riga # datadir = /var/lib/mysql) : bind-address = 127.0.0.1 [LF] skip-networking [LF] sql_mode=NO_ENGINE_SUBSTITUTION [LF] default-authentication-plugin=mysql_native_password [LF] collation-server = utf8_unicode_ci [LF] character-set-server = utf8
  5. (opzionale) Per completezza, al fondo di questo file è presente una configurazione di fine-tuning per un sistema con almeno 16GB di RAM. Volendo, si può direttamente copia-incollare quella configurazione.
  6. Lanciare service mysql restart per applicare le modifiche alla config di Mysql.
  7. Lanciare mysql_secure_installation per creare la password di root per mysql (usare qualcosa di semplice, perchè serve solo localmente), rimuovere l'accesso per gli utenti anonimi, le tabelle temporanee, e disabilitare il login remoto al demone Mysql.
  8. Eseguire mysql -u root e lanciare i comandi di seguito:
  9. CREATE USER 'admin'@'%' IDENTIFIED BY 'x';
  10. GRANT ALL PRIVILEGES ON *.* TO 'admin'@'%' WITH GRANT OPTION;
  11. FLUSH PRIVILEGES;

Ovviamente, le query mysql relative allo username e password da creare come utente privilegiato possono essere modificate. Nell'esempio sopra riportato viene creato un utente con username admin e password x. Ricordarsi che stiamo configurando un ambiente di sviluppo locale, e fintanto che non viene "aperto" al mondo esterno, non dobbiamo preoccuparci di usare politiche corrette per la sicurezza.

Creare dei VirtualHost funzionanti sulla propria installazione locale - Step 3

Per creare dei VirtualHost è sufficiente utilizzare questi due script che velocizzano la configurazione.

Come fase esemplificativa, verrà mostrato come creare un VirtualHost specifico per PhpMyAdmin, che comunque è necessario per poter lavorare.

A questo punto, se ancora loggati come l'utente root, uscire dall'utente root e tornare in modalità utente.

sudo mkdir /etc/apache2/certs2/
cd ~/
mkdir utils
cd utils/
nano create-staging-environment.php (copiare da sotto il contenuto del file)
nano create-ssl-cert.sh (copiare da sotto il contenuto del file)
chmod +x create-ssl-cert.sh
cd ~/
nano lamp.sh (copiare da sotto il contenuto del file)
chmod +x lamp.sh
wget https://files.phpmyadmin.net/phpMyAdmin/5.1.1/phpMyAdmin-5.1.1-all-languages.zip
unzip phpMyAdmin-5.1.1-all-languages.zip
rm -f phpMyAdmin-5.1.1-all-languages.zip
mv phpMyAdmin-5.1.1-all-languages phpmyadmin

Ora abbiamo creato la directory radice per l'installazione di phpmyadmin. Non resta che configurare un VirtualHost funzionante.

Lanciare quindi il comando sudo php create-staging-environment.php e seguire le istruzioni. Queste istruzioni si applicano a tutti i progetti web che si vogliono installare sul sistema.

Nell'esempio, il virtualhost per PhpMyAdmin verrà settato come local.phpmyadmin.biz. Ovviamente, modificare le risposte seguendo il proprio nome utente. Rispondere alle domande dello script come segue:

maurizio@FISSO:~$ cd ~/utils/
maurizio@FISSO:~/utils$ sudo php create-staging-environment.php
### STAGING ENVIRONMENT CREATOR ###
Enter a valid local domain name: local.phpmyadmin.biz
Enter a valid directory in the filesystem for the DocumentRoot: /home/maurizio/phpmyadmin/
Enter a valid PHP version (5.6, 7.1, 7.2, 7.3, 7.4, 8.0 or 8.1): 7.4
Do you need HTTPS support (yes/no/y/n) ? y

Ora, bisogna modificare il file hosts di Windows per inserire il puntamento locale al dominio local.phpmyadmin.biz. Per farlo, modificare il file C:\Windows\System32\drivers\etc\hosts aggiungendo la regola 127.0.0.1 local.phpmyadmin.biz.

Dopodichè, aprire una command line di Windows in modalità privilegiata e lanciare ipconfig /flushdns

Fatto! Ora è possibile navigare sul proprio browser all'indirizzo https://local.phpmyadmin.biz/setup/ per proseguire il setup go PhpMyAdmin.

Per creare altri VirtualHost per altri progetti, utilizzare sempre le stesse istruzioni seguite per il setup di PhpMyAdmin. Basterà far puntare il VirtualHost alla directory giusta del proprio progetto, e definire un nome di dominio fittizio che sarà reindirizzato via HOSTS verso 127.0.0.1

Ottimizzare l'esperienza Linux - Step 4

Per ottimizzare l'installazione LAMP e l'esperienza utente sulla console dei comandi di Linux, seguire questi passaggi:

  1. Seguire le istruzioni di installazione di https://github.com/slomkowski/bash-full-of-colors
  2. Lanciare wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash per installare NVM (per sviluppo NodeJS/React)
  3. Lanciare cd ~/utils/ && php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && php composer-setup.php && php -r "unlink('composer-setup.php');" per installare una versione locale di composer all'interno della directory /utils/
  4. Creare una coppia di chiavi pubblica/privata con il comando ssh-keygen -o -a 100 -t ed25519 -f ~/.ssh/nome_chiave -C "utente@computer"
  5. Comunicare il contenuto della chiave pubblica al proprio team, che la userà per esempio per abilitare l'accesso ad un repository GIT privato.
  6. Copia-incollare il contenuto dello snippet bash_local all'interno di ~/.bash_local modificando la variabile $NOME_UTENTE con il nome dell'utente scelto

Note importanti:

  1. E' possibile fare il reset della macchina Linux con il comando wslrestart che è stato aliasato in ~/.bash_local. Così facendo si resetta completamente il sottosistema WSL2.
  2. Ogni volta che si esegue il login alla macchina virtuale, bisognerà lanciare il comando ~/lamp.sh per avviare i servizi Apache, PHP-FPM e Mysql. Questo è necessario perchè l'ambiente LAMP non è stato installato come servizio di sistema (per intenderci, con systemd).

Installare VS Code per accedere ai file di progetto su WSL2 - Step 5

VS Code è totalmente integrato e compatibile con WSL2, nativamente.

Questo incrementa la produttività e semplifica tantissimo lo sviluppo.

Per installare e configurare VS Code con WSL2 è sufficiente:

  1. Installare VS Code scaricandolo da https://code.visualstudio.com/
  2. Aprire VS Code e premere la combinazione di comandi CTRL + SHIFT + x
  3. Installare l'estensione Remote - WSL
  4. Riavviare VS Code
  5. Aprire una console di Ubuntu, e portarsi su una directory a piacere, ad esempio ~/utils/
  6. Lanciare il comando code . e lasciare che il sistema installi quello che gli serve
  7. Voilà, ora è possibile modificare i file presenti su Ubuntu direttamente da VS Code!

Ottimizzare lo sviluppo web con le estensioni giuste di VS Code - Step 6

Portarsi su una console di Ubuntu e lanciare questi comandi:

  1. cd ~/
  2. mkdir .composer && cd .composer/
  3. nano composer.json e inserire questo contenuto all'interno:
{
	"require": {
		"squizlabs/php_codesniffer": "^3.5",
		"friendsofphp/php-cs-fixer": "^2.16"
	}
}
  1. Lanciare composer install
  2. Ora abbiamo a disposizione i binari di php-cs-fixer e php codesniffer, ci serviranno per la config di VS Code
  3. Aprire VS Code, e portarsi su un progetto residente dentro Ubuntu per rimanere in "modalità WSL2"
  4. Premere CTRL + SHIFT + x, cercare php cs fixer e installare la versione del plugin di junstyle (https://github.com/junstyle/vscode-php-cs-fixer.git)
  5. Installare le seguenti estensioni: GitLens (Eric Amodio, https://github.com/eamodio/vscode-gitlens), Git History (Don Jayamanne, https://github.com/DonJayamanne/gitHistoryVSCode), PHP Intelephense (Ben Mewburn, https://github.com/bmewburn/vscode-intelephense), Prettier - Code Formatter (Prettier, https://github.com/prettier/prettier-vscode), PHP DocBlocker (Nail Brayfield, https://github.com/neild3r/vscode-php-docblocker), Twig Language (mblode, https://github.com/mblode/vscode-twig-language), markdownlint (David Anson, https://github.com/DavidAnson/vscode-markdownlint)
  6. Installare il seguente pacchetto icone: Material Icon Theme (Philipp Kief, https://github.com/PKief/vscode-material-icon-theme)
  7. Premere la combinazione di tasti CTRL + SHIFT + p, digitare preferenze, e cliccare su Preferenze: Apri Impostazioni (JSON)
  8. Copia-incollare la configurazione riportata nello snippet vscode-json-config

Contenuto dello snippet "vscode-json-config"

Ovviamente bisogna modificare la path di php-cs-fixer.executablePath secondo il proprio nome utente.

{
	"window.menuBarVisibility": "toggle",
	"window.zoomLevel": 0.2,
	"explorer.confirmDelete": false,
	"explorer.confirmDragAndDrop": false,
	"workbench.iconTheme": "material-icon-theme",
	"workbench.colorTheme": "Quiet Light",
	"workbench.colorCustomizations": {
		"editorCursor.foreground": "#444"
	},
	"workbench.editor.showTabs": true,
	"terminal.integrated.fontFamily": "monospace",
	"terminal.integrated.lineHeight": 0.8,
	"terminal.integrated.enableBell": false,
	"terminal.integrated.letterSpacing": -0.1,
	"terminal.integrated.fontSize": 16,
	"editor.fontWeight": "600",
	"editor.cursorStyle": "line",
	"editor.cursorBlinking": "phase",
	"editor.rulers": [130],
	"editor.formatOnSave": true,
	"editor.minimap.enabled": false,
	"editor.wordWrap": "on",
	"editor.insertSpaces": true,
	"editor.tabSize": 4,
	"html.format.contentUnformatted": "pre,code,textarea",
	"html.format.endWithNewline": false,
	"html.format.extraLiners": "head, body, /html",
	"html.format.indentHandlebars": false,
	"html.format.indentInnerHtml": false,
	"html.format.maxPreserveNewLines": null,
	"html.format.preserveNewLines": true,
	"html.format.wrapLineLength": 130,
	"html.format.wrapAttributes": "auto",
	"telemetry.enableCrashReporter": false,
	"telemetry.enableTelemetry": false,
	"prettier.trailingComma": "all",
	"prettier.endOfLine": "auto",
	"prettier.singleQuote": true,
	"prettier.useTabs": true,
	"prettier.tabWidth": 4,
	"prettier.printWidth": 130,
	"prettier.jsxBracketSameLine": true,
	"prettier.semi": false,
	"[php]": {
		"editor.formatOnSave": true,
		"editor.defaultFormatter": "junstyle.php-cs-fixer"
	},
	"[js]": {
		"editor.formatOnSave": true,
		"editor.defaultFormatter": "esbenp.prettier-vscode"
	},
	"[javascript]": {
		"editor.formatOnSave": true,
		"editor.defaultFormatter": "esbenp.prettier-vscode"
	},
	"[css]": {
		"editor.formatOnSave": true,
		"editor.defaultFormatter": "esbenp.prettier-vscode"
	},
	"[jsx]": {
		"editor.formatOnSave": true,
		"editor.defaultFormatter": "esbenp.prettier-vscode"
	},
	"[twig]": {
		"editor.formatOnSave": true,
		"editor.defaultFormatter": "mblode.twig-language"
	},
	"php.suggest.basic": false,
	"php.validate.enable": true,
	"typescript.updateImportsOnFileMove.enabled": "never",
	"git.enableSmartCommit": true,
	"git.autofetch": true,
	"git.confirmSync": false,
	"php-cs-fixer.executablePath": "/home/$NOME_UTENTE/.composer/vendor/bin/php-cs-fixer",
	"php-cs-fixer.executablePathWindows": "", //eg: php-cs-fixer.bat
	"php-cs-fixer.onsave": false,
	"php-cs-fixer.rules": "@PSR2",
	"php-cs-fixer.config": ".php_cs;.php_cs.dist",
	"php-cs-fixer.allowRisky": false,
	"php-cs-fixer.pathMode": "override",
	"php-cs-fixer.exclude": [],
	"php-cs-fixer.autoFixByBracket": true,
	"php-cs-fixer.autoFixBySemicolon": false,
	"php-cs-fixer.formatHtml": false,
	"php-cs-fixer.documentFormattingProvider": true,
	"php-cs-fixer.lastDownload": 0,
	"[html]": {
		"editor.defaultFormatter": "esbenp.prettier-vscode"
	},
	"php-docblocker.returnGap": true,
	"php-docblocker.qualifyClassNames": true,
	"php-docblocker.author": {
		"name": "NOME SVILUPPATORE",
		"email": "EMAIL@SVILUPPATORE.BIZ"
	},
	"diffEditor.ignoreTrimWhitespace": false
}

Contenuto dello snippet ".bash_local"

alias casa="cd /home/$NOME_UTENTE"
alias composer="/usr/bin/php7.4 -d allow_url_fopen=1 /home/$NOME_UTENTE/utils/composer.phar"
alias php="/usr/bin/php7.4 -d allow_url_fopen=1 -d memory_limit=1024M"
alias php74="/usr/bin/php7.4 -d allow_url_fopen=1 -d memory_limit=1024M"
alias apt="sudo apt-get"
alias ls="ls -lash --color=auto --group-directories-first"
alias cd..="cd .."
alias ..="cd ../../"
alias ...="cd ../../../"
alias ....="cd ../../../../"
alias .....="cd ../../../../"
alias .4="cd ../../../../"
alias .5="cd ../../../../.."
alias ports="sudo netstat -tulanp"
alias wslrestart="history -a && cmd.exe /C wsl --shutdown"
alias tic="git"
alias npmclean="npm ci"
alias npmcheck="ncu"

gitremove() {
    git branch -d "$@"
    git push origin --delete "$@"
}

hugefiles() {
    du -a "$@" | sort -n -r | head -n 100
}

hugedirs() {
    cd "$@" && du -hsx -- * | sort -rh | head -100
}

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion

Contenuto dello script "lamp.sh"

#!/usr/bin/env bash
echo "### Starting MYSQL 8 ..."
sudo service mysql start
echo "### Starting PHP-FPM 5.6, 7.*, 8.*"
[ -d /var/run/php/ ] || sudo mkdir /var/run/php/
sudo service php5.6-fpm start
sudo service php7.0-fpm start
sudo service php7.1-fpm start
sudo service php7.2-fpm start
sudo service php7.3-fpm start
sudo service php7.4-fpm start
sudo service php8.0-fpm start
sudo service php8.1-fpm start
echo "### Starting Apache Server ..."
sudo service apache2 start

Contenuto dello script "create-staging-environment.php"

IMPORTANTE: Dopo il copia-incolla in create-staging-environment.php, ricordarsi di modificare le stringhe ##NOME_UTENTE## in modo che contengano il nome utente scelto in fase di setup di Ubuntu..

<?php

if (php_sapi_name() !== "cli") {
    message('ERROR: This script can only be run from the CLI.', 'e');
    exit();
}

if (posix_getuid() !== 0) {
    message('ERROR: This script can only be run as ROOT or sudo.', 'e');
    exit();
}

if (!is_dir('/etc/apache2/certs2/')) {
    message('ERROR: You need to create the directory /etc/apache2/certs2/ as root prior of executing this script.', 'e');
    exit();
}

define('APACHE_SITES_AVAILABLE_DIR', '/etc/apache2/sites-available/');
define('APACHE_CERTS_DIR', '/etc/apache2/certs2/');
define('APACHE_CERT_SELFSIGNED_SCRIPT', './create-ssl-cert.sh');
define('PHP_FPM_CONFIG_DIR', '/etc/php/#VER#/fpm/pool.d/');
define('APACHE_MAX_CONF_INDEX', getApacheSitesAvailableMaxIndex());

message('### STAGING ENVIRONMENT CREATOR ###');

// 1. ask for the local domain name
$work = true;
while ($work) {
    $domain = readline('Enter a valid local domain name: ');
    $domain = strtolower(str_replace(['http://', 'https://', 'www.'], '', $domain));
    if (filter_var('http://' . $domain, FILTER_VALIDATE_URL)) {
        $work = false;
        define('DOMAIN', $domain);
    } else {
        message('Error. Retry.', 'e');
    }
}

// 2. ask for the local directory that will be served as the DocumentRoot
$work = true;
while ($work) {
    $document_root = readline('Enter a valid directory in the filesystem for the DocumentRoot: ');
    if (is_dir($document_root)) {
        $work = false;
        define('DOCUMENT_ROOT', rtrim($document_root, '/'));
    } else {
        message('Error. Retry.', 'e');
    }
}

// 3. ask for the PHP version for PHP-FPM
$work = true;
while ($work) {
    $php_version = readline('Enter a valid PHP version (5.6, 7.0, 7.1, 7.2, 7.3, 7.4, 8.0 or 8.1): ');
    if (in_array($php_version, ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1'])) {
        $work = false;
        define('PHPVER', $php_version);
    } else {
        message('Error. Retry.', 'e');
    }
}

// 4. ask for HTTPS support
$work = true;
while ($work) {
    $https_support = readline('Do you need HTTPS support (yes/no/y/n) ? ');
    if (in_array(strtolower($https_support), ['yes', 'no', 'y', 'n'])) {
        $work = false;
        if (in_array(strtolower($https_support), ['yes', 'y'])) {
            define('HTTPS_SUPPORT', true);
        } else {
            define('HTTPS_SUPPORT', false);
        }
    } else {
        message('Error. Retry.', 'e');
    }
}

// 5. perform actions
$apache_conf_stream = getApacheConf();
$php_fpm_conf_stream = getPhpFpmConf();
$apache_conf_file = APACHE_SITES_AVAILABLE_DIR . APACHE_MAX_CONF_INDEX . '-' . DOMAIN . '.conf';
$php_fpm_conf_file = str_replace('#VER#', PHPVER, PHP_FPM_CONFIG_DIR) . DOMAIN . '.conf';

if (file_put_contents($apache_conf_file, $apache_conf_stream)) {
    message('Apache configuration written correctly, going to enable the website', 's');
    exec('a2ensite ' . basename($apache_conf_file));

    if (file_put_contents($php_fpm_conf_file, $php_fpm_conf_stream)) {
        message('PHP-FPM configuration written successfully', 's');

        if (HTTPS_SUPPORT) {
            message('Going to CD into ' . APACHE_CERTS_DIR);
            exec('cd ' . APACHE_CERTS_DIR);
            message('Going to call the self-signed cert script ' . APACHE_CERT_SELFSIGNED_SCRIPT . ' with domain "' . DOMAIN . '"');
            exec(APACHE_CERT_SELFSIGNED_SCRIPT . ' ' . DOMAIN);
        }

        message('Going to exec "service apache2 force-reload"');
        exec('service apache2 force-reload');
        message('Going to exec "service php' . PHPVER . '-fpm restart"');
        exec('service php' . PHPVER . '-fpm restart');
    } else {
        message('Cannot write PHP-FPM configuration', 'e');
    }
} else {
    message('Cannot write Apache configuration', 'e');
}

function message(string $string, string $type = 'i')
{
    switch ($type) {
        case 'e': //error
            echo "\033[31m$string \033[0m\n";
        break;
        case 's': //success
            echo "\033[32m$string \033[0m\n";
        break;
        case 'w': //warning
            echo "\033[33m$string \033[0m\n";
        break;
        case 'i': //info
            echo "\033[36m$string \033[0m\n";
        break;
        default:
            echo "$string\n";
        break;
    }
}

function getApacheSitesAvailableMaxIndex() : string
{
    $files = glob(APACHE_SITES_AVAILABLE_DIR . '*.conf');
    if ($files && count($files) > 0) {
        $max_index = 0;
        foreach ($files as $file) {
            $file = basename($file);
            if (preg_match('/^([0-9]+).*\.conf$/ui', $file, $matches)) {
                $index = intval($matches[1]);
                if ($index >= $max_index) {
                    $max_index = $index;
                }
            }
        }

        return str_pad($max_index + 1, 3, '0', STR_PAD_LEFT);
    }

    return '001';
}

function getApacheConf()
{
    $domain = DOMAIN;
    $document_root = DOCUMENT_ROOT;
    $phpver = PHPVER;
    $newline = "\\n";

    $conf_http = <<<HTML
<VirtualHost 127.0.0.1:80>
    ServerName $domain
    ServerAlias www.$domain
    DocumentRoot $document_root
    ServerAdmin admin@localhost.net
    UseCanonicalName Off
    Options -ExecCGI -Includes
    RemoveHandler cgi-script .cgi .pl .plx .ppl .perl
    CustomLog \${APACHE_LOG_DIR}/$domain combined
    CustomLog \${APACHE_LOG_DIR}/$domain-bytes_log "%{%s}t %I .$newline%{%s}t %O ."

    # Enable HTTP2
    Protocols h2 http/1.1

    <FilesMatch \.php$>
        # Apache 2.4.10+ can proxy to unix socket
        SetHandler "proxy:unix:/var/run/php/php$phpver-fpm-$domain.sock|fcgi://localhost/"
    </FilesMatch>

    <IfModule mod_brotli.c>
        AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css text/javascript application/x-javascript application/javascript application/json application/x-font-ttf application/vnd.ms-fontobject image/x-icon
    </IfModule>

    <Directory $document_root/>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        order allow,deny
        allow from all
        Require all granted
    </Directory>

    ##AUTOREWRITE##
</VirtualHost>

HTML;

    $conf_https = <<<HTML
<IfModule mod_ssl.c>
    <VirtualHost 127.0.0.1:443>
        ServerName $domain
        ServerAlias www.$domain
        DocumentRoot $document_root
        ServerAdmin admin@localhost.net
        UseCanonicalName Off
        Options -ExecCGI -Includes
        RemoveHandler cgi-script .cgi .pl .plx .ppl .perl
        CustomLog \${APACHE_LOG_DIR}/ssl-$domain combined
        CustomLog \${APACHE_LOG_DIR}/ssl-$domain-bytes_log "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"

        # Enable HTTP2
        Protocols h2 http/1.1

        <FilesMatch "\.(cgi|shtml|phtml|php)$">
            SSLOptions +StdEnvVars
        </FilesMatch>

        <Directory $document_root/>
            Options -Indexes +FollowSymLinks
            AllowOverride All
            order allow,deny
            allow from all
            Require all granted
        </Directory>

        <IfModule mod_brotli.c>
            AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css text/javascript application/x-javascript application/javascript application/json application/x-font-ttf application/vnd.ms-fontobject image/x-icon
        </IfModule>

        <FilesMatch \.php$>
            # Apache 2.4.10+ can proxy to unix socket
            SetHandler "proxy:unix:/var/run/php/php$phpver-fpm-$domain.sock|fcgi://localhost/"
        </FilesMatch>

        SSLEngine on
        SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP
        SSLCertificateFile "/etc/apache2/certs2/$domain.crt"
        SSLCertificateKeyFile "/etc/apache2/certs2/$domain.key"
    </VirtualHost>
</IfModule>

HTML;

    if (HTTPS_SUPPORT) {
        return str_replace(
            '##AUTOREWRITE##',
            'RewriteEngine On
            RewriteCond %{HTTPS} off
            RewriteRule ^/(.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]',
            $conf_http
        ) . chr(10) . chr(10) . $conf_https;
    } else {
        return str_replace('##AUTOREWRITE##', '', $conf_http);
    }
}

function getPhpFpmConf()
{
    $domain = DOMAIN;
    $document_root = DOCUMENT_ROOT;
    $phpver = PHPVER;

    $php_fpm_conf = <<<HTML
[$domain]
user = ##NOME_UTENTE##
group = ##NOME_UTENTE##
listen = /var/run/php/php$phpver-fpm-$domain.sock
listen.owner = ##NOME_UTENTE##
listen.group = ##NOME_UTENTE##
php_admin_value[disable_functions] = apache_child_terminate,apache_get_modules,apache_getenv,apache_note,apache_setenv,define_syslog_variables,disk_free_space,diskfreespace,dl,fpassthru,get_current_user,getmyuid,highlight_file,passthru,pclose,pcntl_exec,pfsockopen,php_uname,pcntl_alarm,pcntl_fork,pcntl_get_last_error,pcntl_signal,pcntl_getpriority,pcntl_setpriority,pcntl_strerror,pcntl_signal_dispatch,pcntl_sigprocmask,pcntl_waitpid,pcntl_wait,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_wexitstatus,pcntl_wifexited,pcntl_wifsignaled,pcntl_wifstopped,pcntl_wstopsig,pcntl_wtermsig,popen,show_source,socket_select,socket_strerror,stream_select,syslog,symlink
php_admin_flag[allow_url_fopen] = off
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
chdir = /

catch_workers_output = yes
php_flag[display_errors] = off
php_admin_value[error_log] = $document_root/php$phpver-fpm-errors.log
php_admin_flag[log_errors] = on
php_admin_value[post_max_size] = 128M
php_admin_value[upload_max_filesize] = 128M
php_admin_value[memory_limit] = 1024M
php_value[memory_limit] = 1024M
php_value[short_open_tag] =  On

HTML;

    return $php_fpm_conf;
}

Contenuto dello script "create-ssl-cert.sh"

#!/usr/bin/env bash

# Ask for the base domain
BASE_DOMAIN="$1"
if [ -z "$BASE_DOMAIN" ]; then
  echo "Usage: $(basename $0) <domain>"
  exit 11
fi

fail_if_error() {
  [ $1 != 0 ] && {
    unset PASSPHRASE
    exit 10
  }
}

# Generate a passphrase
export PASSPHRASE=$(head -c 500 /dev/urandom | tr -dc a-z0-9A-Z | head -c 128; echo)

# Days for the cert to live
DAYS=3650

# Generated configuration file
CONFIG_FILE="config.txt"

cat > $CONFIG_FILE <<-EOF
[req]
default_bits = 2048
prompt = no
default_md = sha256
x509_extensions = v3_req
distinguished_name = dn

[dn]
C = IT
ST = Italy
L = Torino
O = Acme Corp
OU = The Dev Team
emailAddress = webmaster@$BASE_DOMAIN
CN = $BASE_DOMAIN

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = *.$BASE_DOMAIN
DNS.2 = $BASE_DOMAIN
EOF

# The file name can be anything
FILE_NAME="$BASE_DOMAIN"

# Remove previous keys
echo "Removing existing certs like $FILE_NAME.*"
if ls /etc/apache2/certs2/$FILE_NAME.* 1> /dev/null 2>&1; then
  rm -rf /etc/apache2/certs2/$FILE_NAME.*
fi

echo "Generating certs for $BASE_DOMAIN"

# Generate our Private Key, CSR and Certificate
# Use SHA-2 as SHA-1 is unsupported from Jan 1, 2017

openssl req -new -x509 -newkey rsa:2048 -sha256 -nodes -keyout "/etc/apache2/certs2/$FILE_NAME.key" -days $DAYS -out "/etc/apache2/certs2/$FILE_NAME.crt" -passin pass:$PASSPHRASE -config "$CONFIG_FILE"

# OPTIONAL - write an info to see the details of the generated crt
openssl x509 -noout -fingerprint -text < "/etc/apache2/certs2/$FILE_NAME.crt" > "/etc/apache2/certs2/$FILE_NAME.info"

# Protect the key
chmod 400 "/etc/apache2/certs2/$FILE_NAME.key"

# Remove the config file
rm -f $CONFIG_FILE

Contenuto del file di config Mysql "mysqld.conf"

Questo file è riportato per comodità. Non è necessario usarlo, ma è fortemente consigliato nel caso si abbia a disposizione una macchina con almenot 16GB di RAM.

#
# The MySQL database server configuration file.
#
# One can use all long options that the program supports.
# Run program with --help to get a list of available options and with
# --print-defaults to see which it would actually understand and use.
#
# For explanations see
# http://dev.mysql.com/doc/mysql/en/server-system-variables.html

# Here is entries for some specific programs
# The following values assume you have at least 32M ram

[mysqld]
#
# * Basic Settings
#
user            = mysql
# pid-file      = /var/run/mysqld/mysqld.pid
# socket        = /var/run/mysqld/mysqld.sock
# port          = 3306
# datadir       = /var/lib/mysql


# If MySQL is running as a replication slave, this should be
# changed. Ref https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_tmpdir
# tmpdir                = /tmp
#
# Instead of skip-networking the default is now to listen only on
# localhost which is more compatible and is not less secure.
bind-address            = 127.0.0.1
skip-networking
sql_mode=NO_ENGINE_SUBSTITUTION
default-authentication-plugin=mysql_native_password
collation-server = utf8_unicode_ci
character-set-server = utf8
#
# * Fine Tuning
#
# sort_buffer_size        = 256000000 (orig)
# key_buffer_size               = 16M (orig)
# max_allowed_packet    = 64M
# thread_stack          = 256K

# thread_cache_size       = -1

# This replaces the startup script and checks MyISAM tables if needed
# the first time they are touched
myisam-recover-options  = BACKUP

# my fine tuning
max_connections          = 20
table_open_cache         = 2048
thread_stack             = 32M
thread_cache_size        = 8
max_allowed_packet       = 16M
max_heap_table_size      = 128M
binlog_cache_size        = 2M
sort_buffer_size         = 16M
join_buffer_size         = 16M
tmp_table_size           = 128M
key_buffer_size          = 512M
read_buffer_size         = 4M
read_rnd_buffer_size     = 32M
bulk_insert_buffer_size  = 64M
myisam_sort_buffer_size  = 128M
innodb_buffer_pool_size  = 2G
innodb_lock_wait_timeout = 150



# table_open_cache       = 4000

#
# * Logging and Replication
#
# Both location gets rotated by the cronjob.
#
# Log all queries
# Be aware that this log type is a performance killer.
# general_log_file        = /var/log/mysql/query.log
# general_log             = 1
#
# Error log - should be very few entries.
#
log_error = /var/log/mysql/error.log
#
# Here you can see queries with especially long duration
# slow_query_log                = 1
# slow_query_log_file   = /var/log/mysql/mysql-slow.log
# long_query_time = 2
# log-queries-not-using-indexes
#
# The following can be used as easy to replay backup logs or for replication.
# note: if you are setting up a replication slave, see README.Debian about
#       other settings you may need to change.
# server-id             = 1
# log_bin                       = /var/log/mysql/mysql-bin.log
# binlog_expire_logs_seconds    = 2592000
max_binlog_size   = 100M
# binlog_do_db          = include_database_name
# binlog_ignore_db      = include_database_name
Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2018, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Una notevole miglioria di PHP7 è senza ombra di dubbio l'inserimento di un operatore di confronto combinato che viene chiamato Spaceship per semplicità comunicativa: infatti è un operatore di confronto che assomiglia tantissimo ad una navetta spaziale stilizzata, un pò come se fosse una emoticon.

Questo operatore è semplicemente definito come <=>

E' un operatore che va utilizzato per la comparazione di scalari, quindi, di numeri interi, bool, float, e stringhe, ma anche per la comparazione di Array e Oggetti. Ecco un esempio:

<?php
echo 1 <=> 1; // 0
echo 1 <=> 2; // -1
echo 2 <=> 1; // 1

La particolarità di questo operatore è che al posto di definire un "vero o falso", come accade per i suoi fratelli <, >, <= e >=, la sua mappa di verità è leggermente diversa, e permette di avere 3 stati di ritorno:

  • 0:  se entrambe le espressioni sono UGUALI
  • 1: se l'espressione di SINISTRA è maggiore
  • -1: se l'espressione di DESTRA è maggiore

Ora, ragionando un secondo su espressioni scalari "stringa", questo operatore fa quello che ha sempre fatto la funzione strcmp di php.

<?php
// qui è chiaramente visibile il comportamento dell'operatore Spaceship, che è assolutamente identico alla funzione strcmp per le stringhe
$a = 'aeroporto';
$b = 'piscina';
echo strcmp ( $a, $b ); // -1
echo $a <=> $b; // -1

La vera potenza di questo operatore è che rende la scrittura di funzioni di ordinamento custom molto più veloce da scrivere: ecco un esempio di stessa identica funzione di ordinamento custom, usando il "vecchio" triplo if, e usando il nuovo operatore Spaceship:

<?php
$numeri = [ 0 => [ 'v' => 10 ], 1 => [ 'v' => 20 ], 2 => [ 'v' => 30 ], 3 => [ 'v' => 10 ], 4 => [ 'v' => 40 ], 5 => [ 'v' => 40 ] ];

// vecchio metodo di ordinamento
usort ( $numeri, function ( $n1, $n2 ) {
   if ( $n1['v'] == $n2['v'] ) return 0;
   else if ( $n1['v'] < $n2['v'] ) return 1;
   else return -1;
});

// nuovo metodo di ordinamento
usort ( $numeri, function ( $n1, $n2 ) {
   return $n1['v'] <=> $n2['v'];
});

Vediamo invece qualche esempio di confronto tra più di un tipo di variabile e i relativi ritorni di verità:

<?php
// Interi
echo 1 <=> 1; // 0
echo 1 <=> 2; // -1
echo 2 <=> 1; // 1
// Float
echo 1.5 <=> 1.5; // 0
echo 1.5 <=> 2.5; // -1
echo 2.5 <=> 1.5; // 1
// Stringhe
echo "a" <=> "a"; // 0
echo "a" <=> "b"; // -1
echo "b" <=> "a"; // 1
echo "a" <=> "aa"; // -1
echo "zz" <=> "aa"; // 1
// Array
echo [] <=> []; // 0
echo [1, 2, 3] <=> [1, 2, 3]; // 0
echo [1, 2, 3] <=> []; // 1
echo [1, 2, 3] <=> [1, 2, 1]; // 1
echo [1, 2, 3] <=> [1, 2, 4]; // -1
// Oggetti
$a = (object) ["a" => "b"];
$b = (object) ["a" => "b"];
echo $a <=> $b; // 0
$a = (object) ["a" => "b"];
$b = (object) ["a" => "c"];
echo $a <=> $b; // -1
$a = (object) ["a" => "c"];
$b = (object) ["a" => "b"];
echo $a <=> $b; // 1
// solo i valori vengono confrontati, non le chiavi: in questo caso i valori sono "b" e "b" in entrambi gli oggetti
$a = (object) ["a" => "b"];
$b = (object) ["b" => "b"];
echo $a <=> $b; // 0

La migliore applicazione di questo comparatore è l'utilizzo in funzioni di ordinamento custom:

<?php
class Veicolo {
   public $nome;
   public $velocitaMassima;
   public function __construct ( string $nome, int $velocitaMassima ) {
      $this -> nome = $nome;
      $this -> velocitaMassima = $velocitaMassima;
   }
}

$veicoli = [
   new Veicolo ( 'Panda', 160 );
   new Veicolo ( 'Punto', 180 );
   new Veicolo ( 'Giulia', 240 );
   new Veicolo ( 'Ape', '50' );
   new Veicolo ( 'Ducato', 160 );
   new Veicolo ( 'Z4', 240 );
];

// ordinamento per nome
usort ( $veicoli, function ( $v1, $v2 ) {
   return $v1 -> nome <=> $v2 -> nome;
});
echo $veicoli[0] -> name;  // Ape

// ordinamento per velocità massima
usort ( $veicoli, function ( $v1, $v2 ) {
   return $v1 -> velocitaMassima <=> $v2 -> velocitaMassima;
});
echo $veicoli[0] -> name;  // Giulia

Vediamo come si comporta l'operatore Spaceship di PHP 7 quando utilizzato su ordinamenti di valori multipli. Questa tecnica semplifica enormemente l'ordinamento di array o oggetti basando l'ordinamento su più chiavi.

<?php
usort ( $veicoli, function ( $v1, $v2 ) {
   return [ $v1 -> velocitaMassima, $v1 -> nome ] <=> [ $v2 -> velocitaMassima, $v2 -> nome ];
});
foreach ( $veicoli as $veicolo ) {
   echo 'La velocità massima di "' $veicolo -> nome . '" è ' . $veicolo -> velocitaMassima . '\n';
}
// Stampa:
// La velocità massima di "Ape" è 50
// La velocità massima di "Ducato" è 160
// La velocità massima di "Panda" è 160
// La velocità massima di "Punto" è 180
// La velocità massima di "Giulia" è 240
// La velocità massima di "Z4" è 240

Sfruttando la comparazione di array, questa funzione di ordinamento ha richiesto 1 sola riga per essere scritta, al posto delle 6 o 7 richieste precedentemente.

Questa era la seconda lezione su PHP 7. Naviga nella categoria PHP7 di questo blog per scoprire le altre lezioni e le novità introdotte da PHP7.

Ragionando in PHP 7 Lezione 1: Tipizzazione scalare

Postato in PHP 7 | Programmazione PHP
Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2018, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

PHP 5 ha introdotto a suo tempo la possibilità per lo sviluppatore di definire verbosamente i tipi di dato richiesti da una funzione. Questo impedisce l'esecuzione di funzioni o metodi richiamati con tipi di dati errati, come ad esempio passare un timestamp UNIX a un metodo che si aspetta un oggetto DateTime. In più, la tipizzazione rende anche chiaro agli altri sviluppatori come utilizzare la funzione in piena compatibilità. Per esempio, qui sotto, si analizzi la definizione delle due funzioni con e senza tipizzazione:

// Senza tipizzazione
function prossimoGiornoSettimana($data) { /*...*/ }
// Con tipizzazione (PHP5)
function prossimoGiornoSettimana(DateTime $data) { /*...*/ }

La tipizzazione era inizialmente limitata alle sole classi e interfacce, ma fu poi ampliata per consentire anche i tipi Array e Callable. PHP 7, invece, estende ulteriormente questo limite consentendo la tipizzazione scalare come int, float, string e bool:

// Tipizzazione scalare (PHP7)
// è possibile definire il tipo richiesto per il parametro $data come INT. E' chiaramente comprensibile allo sviluppatore che questa funzione accetta un timestamp.
function prossimoGiornoSettimana(int $data) { /*...*/ }

Controllo della tipizzazione

Il controllo di tipizzazione per i tipi scalari in PHP 7 funziona in modalità "Coercive", ovvero, nella stessa identica maniera con la quale le vecchie versioni di PHP effettuano il controllo di tipizzazione sulle funzioni built-in. Per meglio spiegare questo concetto, basta pensare alla situazione della funzione floor, che è così definita:

// definizione della funzione float built-in di PHP
float floor ( float $numero )

Quando si passa un parametro alla funzione float, PHP effettuerà una conversione "Coercive" del valore passato alla funzione, se questa è convertibile. Esiste una tabella di conversione per capire se un tipo di variabile può essere automaticamente convertito da PHP in modalità "Coercive":

Tipo di datointfloatstringboolobject
intsisi*si**sino
floatsisisi*sino
stringsisisisisi***
intsisisisino

*: Solo valori che non siano NaN e compresi tra PHP_INT_MIN e PHP_INT_MAX
**: solo se è una stringa numerica
***: solo se l'oggetto ha un metodo magico __toString()

PHP 7 introduce un nuovo metodo per il controllo della tipizzazione, chiamato "Strict", che permette allo sviluppatore di definire, all'inizio dello script, una particolare "frase" che, se presente, imporrà al compilatore di effettuare il controllo di tipizzazione in senso stretto, quindi senza interpolazione dei parametri "Coercive" come sopra spiegato. Attenzione: utilizzare il controllo di tipizzazione Strict farà in modo che anche eventuali funzioni built-in di PHP richiamate all'interno del file saranno sottoposte allo stesso identico trattamento di validazione.

La modalità Strict essenzialmente richiede che tutte le variabili passate alle funzioni tipizzate siano in regola con la definizione della funzione/metodo. In caso contrario, verrà lanciato un TypeError. Ad esempio, in modalità Strict, non è possibile utilizzare stringhe numeriche quando la tipizzazione richiede un float o un int (come invece in modalità Coercive è ammesso e funzionante):

<?php
declare(strict_types=1);
function ciao(string $nome): string {
   echo 'Ciao, ' . $name;
}
function somma(float $a, float $b): float {
   return $a + $b;
}
ciao('Alberto'); // Stampa: "Ciao, Alberto"
ciao(3); // genera un Fatal error: Uncaught TypeError: Argument 1 passed to ciao() must be of the type string, integer given
somma(2, 1); // ritorna "3" anche se i tipi passati sono int
somma(3.2, true); // genera un Fatal error: Uncaught TypeError: Argument 2 passed to somma() must be of the type float, bool given

Poiché la direttiva è impostata per ogni file, e ad inizio di ogni file, è possibile e ammesso combinare le modalità Strict e Coercive nell'applicazione. Ad esempio, se alcune parti della propria applicazione ( magari installate tramite Composer ) non richiedono tipizzazione stretta, mentre invece alcuni specifiche classi della propria applicazione la richiedono, tutta l'applicazione continuerà a funzionare, e il controllo di tipizzazione stretto verrà applicato solo ed esclusivamente sui file che ne fanno uso.

Compatibilità con versioni di PHP precedenti

Non è più possibile, con PHP 7, creare classi denominate int, float, string o bool in quanto queste andrebbero in conflitto con le nuove tipizzazioni di tipo scalare.

Questa era la prima lezione su PHP 7. Naviga nella categoria PHP7 di questo blog per scoprire le altre lezioni e le novità introdotte da PHP7

Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2018, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

JSON5 è una nuova versione dello standard JSON, che aggiunge alcune peculiarità alle specifiche originali dello standard, rendendolo un pò più developer-friendly: sono infatti state introdotte alcune novità interessanti, di cui ne cito alcune:

  1. Possibilità di inserire commenti dentro il codice JSON
  2. Compatibilità con i cosiddetti "trailing comma", ovvero, non bisogna dannarsi l'anima per eliminare tutte le virgole in eccesso al termine di un array
  3. Le stringhe possono essere istanziate anche con i single quotes '
  4. I numeri possono essere definiti anche con Infinity, -Infinity, NaN, e -NaN
  5. I numeri possono essere definiti anche come esadecimali

Tutti i dettagli di implementazione sono disponibili sulla pagina ufficiale di JSON5.

Come fare quindi per utilizzare tutte le potenzialità di JSON5 anche su PHP?

Ci viene in aiuto una comoda libreria installabile via composer, colinodell/json5 (link a github)

E' sufficiente integrarla all'interno del vostro progetto con composer:

composer require colinodell/json5

Da questo punto in poi, è possibile utilizzare, al posto delle funzioni json_encode () e json_decode (), i corrispettivi json5_encode () e json5_decode (), che funzionano con gli stessi identici parametri rispetto al passato. Quindi, è possibile effettuare una migrazione di tutto il vostro codice sorgente compatibile con json, semplicemente facendo un search-and-replace globale sulle funzioni json_encode e json_decode, rimpiazzandole con le stringhe nuove.

Ecco un esempio di codice JSON5

{
    foo: 'bar',
    while: true,

    this: 'is a \
multi-line string',

    // this is an inline comment
    here: 'is another', // inline comment

    /* this is a block comment
       that continues on another line */

    hex: 0xDEADbeef,
    half: .5,
    delta: +10,
    to: Infinity,   // and beyond!

    finally: 'a trailing comma',
    oh: [
        "we shouldn't forget",
        'arrays can have',
        'trailing commas too',
    ],
}
Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2018, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Molte volte si presenta la necessità di dover modificare una parte del codice HTML di un sito Wordpress. Le strategie per farlo sono molte, a partire dalla modifica diretta dei file di template, passando per la creazione di child template basati sul template originale, e ancora hook su plugin già presenti per modificarne le funzionalità.

Tuttavia, tutte le opzioni sopra riportate sono scomode, controintuitive, ed esose in termini di tempo.

Consideriamo una modifica molto banale al codice HTML finale di un sito Wordpress, ovvero, la riscrittura di tutte le frasi "Read more" oppure "Error page". Questa necessità capita quasi sempre quando si installa un template da themeforest: il supporto alla traduzione è spesso incompleto o parziale, ed andare ad agire sui file di traduzione è noioso e fa perdere tempo.

Fortunatamente, esiste un metodo per "agguantare" il codice HTML del sito prima che venga mandato in output ai browser client, ricalcolarlo a proprio piacimento, e mandarlo in output modificato così come è stato da noi definito.

Tutto questo è fattibile con un semplicissimo plugin, che incollo qui sotto. Ovviamente, questa soluzione è "quick and dirty". Non è da considerarsi lo stato dell'arte per effettuare modifiche al behaviour del proprio template. E' da usare solo quando si ha fretta, o il template è davvero troppo complicato o vetusto da impedirne una rapida modifica.

<?php
/*
Plugin Name: Final Output Buffer
Plugin URI: https://www.mauriziofonte.it/
Description: Il plugin definitivo per la modifica dell'output HTML di Wordpress.
Version: 1.0
Author: Maurizio Fonte
Author URI: https://www.mauriziofonte.it/
License: Questo plugin è concesso con licenza GPL
*/

function wp_mf_is_login_page ( ) {
    return in_array ( $GLOBALS['pagenow'], array('wp-login.php', 'wp-register.php' ) );
}

function wp_mf_finaloutputbuffer ( $buffer ) {
	
	if ( is_admin() || wp_mf_is_login_page () ) return $buffer;
	
	// correzione di tutti i testi "Read More"
	$buffer = str_ireplace ( 'read more', 'Continua a leggere...', $buffer );
	
	// correzione di tutti i testi "Related posts"
	$buffer = str_ireplace ( 'related posts', 'Articoli correlati', $buffer );
	
	// correzione di tutti i testi "goto homepage"
	$buffer = str_ireplace ( 'goto homepage', 'Ritorna alla Home', $buffer );
	
	// correzione di tutti i testi "error 404"
	$buffer = str_ireplace ( 'error 404', 'Errore 404', $buffer );
	
	return $buffer;
}

function wp_mf_finaloutputbuffer_buffer_start() { 
	if ( ! is_admin() && ! wp_mf_is_login_page () ) ob_start ( "wp_mf_finaloutputbuffer" ); 
}

function wp_mf_finaloutputbuffer_buffer_end() { 
	if ( ! is_admin() && ! wp_mf_is_login_page () ) {
		$html = ob_get_clean ();
		echo $html;
	}
}

add_action ( 'after_setup_theme', 'wp_mf_finaloutputbuffer_buffer_start' );
add_action ( 'shutdown', 'wp_mf_finaloutputbuffer_buffer_end' );

Questo codice va inserito all'interno di un file PHP che chiamerete "final-output-buffer.php", ed inserito dentro una cartella, chiamata anch'essa "final-output-buffer", creata all'interno della cartella "wp-content/plugins" della vostra installazione di Wordpress.

Questo plugin funziona nel seguente modo:

  1. Aggiunge un hook dopo il caricamento del template, wp_mf_finaloutputbuffer_buffer_start
  2. Aggiunge un hook su completamento di tutte le azioni di Wordpress, wp_mf_finaloutputbuffer_buffer_end
  3. Istanzia l'output buffer di PHP dichiarando come callback la funzione wp_mf_finaloutputbuffer

Quindi, il flow di esecuzione è il seguente:

  1. Wordpress fa il setup dei plugin e dei template
  2. A completamento della 1), triggera l'esecuzione di wp_mf_finaloutputbuffer_buffer_start, che controlla che la pagina attualmente "in lavorazione" NON sia una pagina di amministrazione o la pagina di login: nel qual caso, procede come se questo plugin non esistesse, altrimenti, istanzia l'output buffer con callback su wp_mf_finaloutputbuffer
  3. Il sito costruisce il "proprio" HTML ignaro di quello che farà wp_mf_finaloutputbuffer
  4. Il codice HTML "originale" viene agganciato da wp_mf_finaloutputbuffer, che lo ricalcola sulla base del codice PHP di replace che è stato inserito dallo sviluppatore
  5. Viene richiamata la funzione wp_mf_finaloutputbuffer_buffer_end, che manda in output il codice HTML sulla base del ricalcolo della 4)

Per modificare il plugin secondo le proprie necessità, è sufficiente andare a modificare la funzione wp_mf_finaloutputbuffer, andando ad effettuare preg_match, str_replace, o qualsiasi tipo di funzione di manipolazione di stringhe su $buffer, che contiene tutto il codice HTML.

Se nella vostra installazione dovessero già essere presenti dei plugin che sfruttano questa tecnica, è molto probabile che avrete dei problemi di content encoding, o che il sito mostrerà pagina bianca. Se dovesse succedere, è sufficiente rinominare temporaneamente la cartella "final-output-buffer", in modo che Wordpress disattivi il plugin, e andare ad analizzare la situazione successivamente.

Effettuare upgrade a PHP 7.2

Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2018, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Con l'avvento di PHP 7.0 il web è diventato più veloce. Molto più veloce. Oltre a tutte le facilitazioni per lo sviluppatore relative al linguaggio in senso stretto, l'incremento di prestazioni nella major release 7 di PHP è davvero notevole, ed è visibile ad occhio nudo, sia utilizzando CMS molto abusati come Wordpress sia utilizzando codice senza framework o script ad-hoc. Si capisce al primo colpo che la propria applicazione sta performando meglio.

Qui sotto riporto una immagine rappresentativa di test basati solo ed esclusivamente sul profilo computazionale delle versioni di PHP dalla 5.0.5 alla 7.1 effettuate da

sudo add-apt-repository ppa:ondrej/php
sudo apt-get update

Istruzioni per PHP 7.2 per Debian 8 (Jessie) e 9 (Stretch)

sudo apt-get install apt-transport-https lsb-release ca-certificates
sudo wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg
echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/php.list
sudo apt-get update

Questi comandi aggiungeranno il repository PPA di Ondřej Surý's, che si occupa di mantenere i pacchetti di PHP 7.2 (oltre a moltre altre versioni) e renderlo disponibile per molte versioni di Linux.

Prima di passare alla installazione vera e propria, però, con i relativi comandi, è necessario fare un pò di attenzione preliminare.

Prima di tutto, create una lista di tutti i pacchetti aggiuntivi di PHP che avete installato sul vostro sistema, come ad esempio php-mbstring o php-curl o php-intl . Questo è necessario perchè poi dovrete reinstallare i corrispettivi in versione 7.2.

In genere, per una normale installazione LAMP, quindi PHP con Apache e Mysql, che supporti la quasi totalità dei requisiti minimi richiesti dai comuni CMS, è necessario installare questi pacchetti: libapache2-mod-php7.2 php7.2 php7.2-common php7.2-gd php7.2-mysql php7.2-imap php7.2-cli php7.2-cgi php7.2-curl php7.2-intl php7.2-pspell php7.2-recode php7.2-sqlite3 php7.2-tidy php7.2-xmlrpc php7.2-xsl php7.2-zip php7.2-mbstring php7.2-soap

Ad ogni modo, io consiglio di operare in questa maniera: dapprima disinstallare tutti i pacchetti di PHP attualmente presenti sul sistema, lasciare che apt ci dica quali pacchetti sta per disinstallare, e poi digitare manualmente la lista di pacchetti da installare andando a leggere quelli che sono stati disinstallati, usando le nuove versioni "-7.2".

Per disinstallare tutto ciò che riguarda PHP dal sistema è sufficiente digitare questo comando:

sudo apt-get remove --purge php*

Poi, analizzare i pacchetti disinstallati, ed installare tutte le rispettive versioni "7.2". Sopra ho riportato la lista di pacchetti di base che vanno bene per la maggior parte dei casi, però, è comunque necessario porre attenzione a quello che si sta facendo, in quanto questo update non è un semplice "aggiorna e dimentica" ma si tratta di un aggiornamento a pacchetti non ufficiali e che potrebbe rendere instabile o non funzionante il vostro ambiente.

sudo apt-get install libapache2-mod-php7.2 php7.2 php7.2-common php7.2-gd php7.2-mysql php7.2-imap php7.2-cli php7.2-cgi php7.2-curl php7.2-intl php7.2-pspell php7.2-recode php7.2-sqlite3 php7.2-tidy php7.2-xmlrpc php7.2-xsl php7.2-zip php7.2-mbstring php7.2-soap

Testare il vostro sistema dopo l'installazione, non solo andando ad aprire sul browser la vostra applicazione, ma andando a leggere i log di errore di PHP e Apache è un obbligo. Lì potrete capire se c'è qualche pezzo del software non compatibile con la nuova versione, o se avete dimenticato un pacchetto aggiuntivo di PHP che ne estende le funzionalità.

Cloud aziendale (quasi) gratis con OwnCloud

Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2016, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

I servizi di Cloud Storage, come ad esempio Dropbox o Google Drive si sono affermati nel corso degli anni come degli strumenti praticamente indispensabili per la informatizzazione dei processi aziendali, e per la condivisione di informazioni tra colleghi e professionisti.

L'unico grosso problema di questi servizi, è che con il crescere dell'operatività aziendale, con il crescere delle persone che utilizzano contemporaneamente il servizio, e con il crescere dell'esigenza di informatizzare tutti i processi aziendali, lo spazio di archiviazione diventa un fattore di criticità, perchè il prezzo di questi servizi aumenta considerevolmente aumentando lo spazio ( e il numero di utenti simultanei ) richiesto.

Giusto per fare un esempio, Google Drive costa circa 2 euro al mese per 100GB di spazio, e il piano tariffario subito successivo obbliga ad acquistare 1TB di storage al costo di circa 10 euro al mese.

Quindi, come si può implementare una soluzione più economica e più scalabile per cloudicizzare l'operatività?

Esiste un software opensource di nome OwnCloud, che mette a disposizione sia il software necessario per l'installazione del "core" su un server Linux, sia i client windows, mac e linux per collegare appunto il server alle proprie sincronizzazioni.

In più, una volta installato il Core sul proprio server, OwnCloud mette a disposizione una interfaccia grafica wia web davvero professionale e accattivante, che servirà all'amministratore per configurare l'ambiente di condivisione, per creare utenti e grupppi, e configurare le impostazioni di base come quelle del relay di posta per le notifiche.

Quindi, quali sono i passaggi per installare e configurare OwnCloud? Ecco una breve guida per l'installazione di OwnCloud

  • 1. Acquistare un server Linux economico con abbastanza spazio. Per questo consiglio caldamente l'acquisto di un server su Backupsy. Con circa 5.5 euro al mese si può acquistare un server da 250GB con una versione di Linux a piacimento. E' caldamente consigliato l'utilizzo di Debian o Ubuntu.
  • 2. Configurare il server LAMP ( Apache, Mysql, PHP ) oppure LEMP ( Nginx, Mysql, PHP ) per poter ospitare correttamente l'installazione di OwnCloud. Attenzione: in caso di utilizzo di Debian 7, l'installazione del server LAMP sarà un pò più complessa perchè il repository ufficiale di Debian 7 mette a disposizione PHP versione 5.4, mentre per l'installazione di OwnCloud è caldamente consigliabile utilizzare una versione di PHP maggiore o uguale alla 5.6, sebbene OwnCloud supporti i sistemi PHP dalla 5.4 in poi. Il fatto di avere una installazione LAMP con una versione di PHP più aggiornata è derivato dal fatto che gli aggiornamenti di PHP non solo correggono delle grosse falle di sicurezza, ma sono molto più performanti.
  • 3. Configurare PHP installando queste estensioni: Ctype, Dom, GD, Iconv, JSON, LibXml, mbstring, Posix, SimpleXML, XMLWriter, Zip, Zlib. Inoltre, è caldamente consigliato installare anche i moduli APC o APCu ( sono delle estensioni di Memory Caching per PHP ). Se avrete bisogno anche di funzionalità di anteprima dei file multimediali all'interno di OwnCloud, come ad esempio le immagini e i file video / audio, dovrete anche occuparvi dell'installazione dei moduli Imagick di PHP, FFMPEG e OpenOffice sul sistema.
  • 4. Configurare un VirtualHost con un dominio esistente e raggiungibile da server DNS ( ci servirà per la configurazione dell'HTTPS ). Ad esempio, se la vostra azienda ha un sito internet con nome di dominio www.aziendapincopallo.it, vi consiglio di creare un sottodominio, ad esempio cloud.aziendapincopallo.it, e configurare il VirtualHost sul nuovo server in modo che risponda a questo sottodominio. Attenzione alle configurazioni che permettono l'accesso diretto via indirizzo IP del server. I VirtualHost sono da configurare come si deve.
  • 5. Installare un certificato SSL gratis tramite l'utilizzo di Let's Encrypt. Nella fattispecie, dovrete utilizzare il tool certbot che si occuperà di configurarvi automaticamente il server Apache ( con il virtualhost configurato correttamente ). L'utilizzo di SSL è seriamente e caldamente consigliato perchè, in caso di assenza, le password di autenticazione tra i Client di sincronizzazione e il Server con il "core" di OwnCloud viaggerebbero in chiaro sulla rete Internet. Quindi, un qualsiasi man in the middle, anche con pochissima esperienza in hacking, potrebbe prendere il controllo del vostro ambiente Cloud, con tutte le conseguenze del caso.
  • 6. Completate l'installazione di OwnCloud andando con il browser sulla posizione di installazione, e configurare i parametri di connessione mysql. Ovviamente, non c'è neanche da ricordarlo, durante tutte le fasi di configurazione, usare sempre password CASUALI maggiori di 18 caratteri.
  • 7. Installare i Client di sincronizzazione sul vostro PC, Windows Mac o Linux che sia. Per la visualizzazione in mobilità, invece, consiglio l'utilizzo di una applicazione per Android che si chiama Solid Explorer. E' un file viewer per Android, che però permette di inserire delle "cartelle virtuali" come se esistessero all'interno del vostro smartphone. Tra le tante opzioni per la creazione di cartelle virtuali, tra cui Dropbox, Google Drive, Amazon S3, c'è anche la possibilità di inserire un server OwnCloud. Vi basterà quindi semplicemente digitare l'indirizzo di installazione ( in https ) del vostro OwnCloud, username e password, e avrete a disposizione tutti i vostri file.

E' chiaro che l'installazione e la configurazione sono fattibili solo da persone che hanno buone competenze in Linux e in amministrazione di sistemi LAMP, ma con un pò di tempo e pazienza, e leggendo qualche guida, è fattibile da chiunque.

Nel caso in cui tu stia valutando l'opportunità di usare OwnCloud per la tua azienda, e avessi bisogno di consulenza PHP a Torino o mysql database administrator a Torino, contattami usando il modulo presente in questo sito.

Fare porting dalle funzioni mysql alle mysqli

Postato in Programmazione PHP
Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2016, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Le storiche funzioni mysql_ di PHP sono diventate deprecate dalla versione 5.5.0, e sono state completamente rimosse dalla nuovissima versione di PHP 7.0 che ha fatto la sua nascita alla fine del 2015.

Quindi, dato che la maggior parte del vecchio codice sorgente di quasi tutti i siti o le applicazioni web "hand-made" contengono in misura piàù o meno pesante queste chiamate a funzione mysql, bisogna trovare un modo veloce per eseguire dei "refactoring completi" e arginare il problema prima che php 7.0 diventi lo standard.

Tra l'altro, queste funzioni deprecate riempiono i log degli errori di PHP di stringhe di questo tipo:

mysql_connect(): The mysql extension is deprecated and will be removed in the future: use mysqli or PDO instead

Ricordatevi che proprio tutte le funzioni del vecchio mysql_ sono diventate deprecate, quindi mi riferisco a mysql_connect, mysql_query, mysql_select_db, mysql_fetch_assoc e via dicendo. Tutto il vecchio armamentario mysql.

Fortunatamente i bravi coder in PHP ( in questo caso, più scripter che coder ) hanno già trovato e scritto una soluzione, che è disponibile su GitHub: MysqlConverterTool su GitHub ( fork )

Una volta scaricata ( Link al Master su GitHub ) è da scompattare dove più preferite sul vostro sistema. Dispone sia di una GUI sia di una interfaccia a riga di comando per impostarne i parametri. In questa guida utilizzeremo la modalità a riga di comando, più comoda per gli smanettoni su *nix.

cd MySQLConverterTool-master
php -q cli.php

Questo comando senza parametri ci spiega come utilizzare lo strumento:

Usage of cli.php :

-f <file>         Convert file
-d <directory>    Convert directory
-p <pattern>      File name pattern for -d, e.g. -p "*.php,*.php3". Default: *
-s <code>         Convert code snippet

-u                Update (modify) input file during the conversion
-b                Backup files to [original_name].org before they get updated

-v                verbose - print conversion details
-w                warnings - print errors/warnings, if any
-q                quiet - don't print the generated code

Quindi, dopo che abbiamo compreso le istruzioni, possiamo mandare il comando

php cli.php -d /home/maurizio/public_html -p "*.php" -u -q

A questo punto, il tool manderà in output sulla riga di comando tutti gli script modificati. Adesso quindi, è meglio controllare se l'applicazione sta funzionando. Molto probabilmente il tool avrà dimenticato qualcosa, come nel mio caso, nel quale in una applicazione molto vetusta erano presenti delle chiamate alla funzione mysql_result. Per aggiustare il problema, mi è bastato testare l'applicazione, analizzare il file di log degli errori di PHP segnando le righe degli script che causavano l'eccezione sulla mysql_result, e poi andare a integrare nell'applicazione una piccola funzione che fa da compatibilità tra la vecchia mysql_result e il nuovo mysqli.

function mysqli_result($res,$row=0,$col=0){ 
	$numrows = mysqli_num_rows($res); 
	if ($numrows && $row <= ($numrows-1) && $row >=0){
		mysqli_data_seek($res,$row);
		$resrow = (is_numeric($col)) ? mysqli_fetch_row($res) : mysqli_fetch_assoc($res);
		if (isset($resrow[$col])){
			return $resrow[$col];
		}
	}
	return false;
}

A questo punto, basta modificare manualmente tutte le chiamate alla funzione mysql_result, usando appunto la mysqli_result, con gli stessi identici parametri.

Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2016, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

You surely have experienced a lot of pain when handling your backups coming from the various servers you have online. A very important thing to remember is, in fact, to have backups of your live web servers, so if things go crazy, you have the "last resource".

Plus, backups should be kept for at least some days, so that if you find out a web server breach or something you haven't noticed before, you can roll back the situation to the backup of N-days before.

If you're running cPanel, getting automated backups is actually pretty easy and straightforward. With cPanel you can even have a default backup retention period of N days, and retain that backups directly on the main system. But, that retention period is relative to the HDD of the main server you're running on. And, another negative fact is, if your main cPanel server that keeps your 10-days old backups fails or gets destroyed by the sys admin?

This is why, in my opinion, the best thing to do is have an external backup server where you send, via FTP, every type of backup from the webservers you are currently running.

Having an external backup server also enables you to have more than one server to be backed-up. Personally, I run a backup server that accepts backups from 3 different cPanel servers sources.

To help your backup server, and to help you setting everything up quick, I've coded a little script, that can be cron-jobbed and run via PHP CLI, that reads the backup destination directory ( the one into which you make your remote cPanel FTP send into ) and organizes the files contained into it by day, purging the backups that are too old to be retained.

If you're looking at guides on how to turn on automated cPanel backup and FTP remote send, there are plenty of guides and walkthroughs. Google is you friend.

Let's have a look at the code:

<?php
	/*
	|--------------------------------------------------------------------------
	| PHP / CGI Automated "Backup File Rotation" script
	|--------------------------------------------------------------------------
	|
	| Version: 1.1.0
	| Author: Maurizio Fonte
	| Author URL: https://www.mauriziofonte.it
	| Description:
	|     This script ( that needs to be run via CLI e.g /usr/bin/php -q this_script.php ) serves as an automated backup rotation organization.
	|     In fact, it takes all subdirectories under the main BACKUP_DIR folder and copies them to another location in which these backup files will be stored and rotated.
	|     For example, consider these directories and their contents:
	|         
	|     BACKUP_DIR = /root/backup
	|
	|             /root/backup
	|                 |-> folder_a
	|                 |       -> folder_a_backup_file_1.tar.gz
	|                 |       -> folder_a_backup_file_2.tar.gz
	|                 |-> folder_b
	|                 |       -> folder_b_backup_file_1.tar.gz
	|                 |       -> folder_b_backup_file_2.tar.gz
	|                 |       -> folder_b_backup_file_3.tar.gz
	|                 |-> folder_c
	|                 |       -> folder_c_backup_file_1.tar.gz
	|
	|    For every execution day, the script will read the contents of the /root/backup folder, and re-organize the files under the directory /back_rot/
	|    The script will create this rotation structure:
	|
	|    BACKUP_ROTATION_DIR = /back_rot/
	|
	|        /back_rot/
	|             |-> 2016-01-01
	|                 |-> folder_a
	|                 |       -> folder_a_backup_file_1.tar.gz
	|                 |       -> folder_a_backup_file_2.tar.gz
	|                 |-> folder_b
	|                 |       -> folder_b_backup_file_1.tar.gz
	|                 |       -> folder_b_backup_file_2.tar.gz
	|                 |       -> folder_b_backup_file_3.tar.gz
	|                 |-> folder_c
	|                 |       -> folder_c_backup_file_1.tar.gz
	|             |-> 2016-01-02
	|                 ...
	|                 ...
	|
	|    After a backup file / folder in the BACKUP_ROTATION_DIR becomes "too old to survive" ( after BACKUP_RETENTION_PERIOD days ), the folder will be automatically purged.
	|    
	|    Please consider that, in order for this automated backup rotator to work *properly*, these conditions have to be met:
	|        1) Each day, some other script ( from a remote server, for example ) has to fill its own directory on BACKUP_DIR ( in the example provided, "folder_a", "folder_b", "folder_c" )
	|        2) This script needs to be run once per day ( nothing harmful will happen if you run this more than once per day, though. Simply, it will see there are no actions to do ).
	|        3) This script is meant to be executed via CLI
	*/
	@ini_set('max_execution_time', 300);
	@ini_set('memory_limit', '256M');
	date_default_timezone_set ( 'Europe/Rome' );
	define ( 'HOME_DIR', rtrim ( dirname ( __FILE__ ), '/' ) . '/' );
	define ( 'LOGS_DIR', 'logs/' );
	define ( 'BACKUP_DIR', '/BACKUPS/' );
	define ( 'BACKUP_ROTATION_DIR', '/BACKUPS_ROTATION/' );
	define ( 'BACKUP_RETENTION_PERIOD', 20 );
	define ( 'EXCLUDE_BACKUP_FILENAME', 'exclude_this_backup_filename.tar.gz' );
	define ( 'EXCLUDE_BACKUP_FOLDER', 'exclude_this_directory_name_while_parsing' );
	
	ob_start ();
	out ( '#############################################################################################' );
	out ( '#############                                                                   #############' );
	out ( '#############     Automated PHP/CGI BACKUP FILE ROTATION ALGORITHM  v1.1.0      #############' );
	out ( '#############       Copyright(c) 2016 Maurizio Fonte - mauriziofonte.it         #############' );
	out ( '#############                                                                   #############' );
	out ( '#############################################################################################' );
	out ( '###############                   ' . date ( 'Y-m-d H:i:s' ) . '                         ###############' );
	out ( '#############################################################################################' );
	
	// fase 0: se non esiste la cartella BACKUP_DIR, non possiamo fare ovviamente nulla...
	if ( ! is_dir ( BACKUP_DIR ) ) closenow ( true, 'This magic script can't do anything as long BACKUP_DIR does not exists ... actual BACKUP_DIR is "' . BACKUP_DIR . '"' );
	
	// fase 1: controllo che la directory dei backup giornalieri e dei backup rotation sia leggibile e scrivibile e mi pre-carico i suoi contenuti in un semplice array di file
	out ( ' ** Starting "' . BACKUP_DIR . '" recursive directory iterator...' );
	$backup_dir_contents = Array ();
	$objects = new RecursiveIteratorIterator ( new RecursiveDirectoryIterator ( BACKUP_DIR ), RecursiveIteratorIterator::SELF_FIRST );
	if ( $objects ) {
		foreach ( $objects as $name => $object ){
			if ( $name !== '.' && $name !== '..' ) {
				$name = realpath ( $name );
				if ( ! in_array ( $name, $backup_dir_contents ) && $name !== '/' && $name !== rtrim ( BACKUP_DIR, '/' ) ) $backup_dir_contents[] = $name;
			}
		}
	}
	
	// fase 1a: controllo che abbiamo realmente qualcosa da fare dentro $backup_dir_contents e creo le directory di destinazione, se non esistono
	if ( count ( $backup_dir_contents ) == 0 ) closenow ( true, 'Nothing found on "' . BACKUP_DIR . '" that can be eligible to a copy/paste! Is that directory empty?' );
	
	if ( ! is_dir ( BACKUP_ROTATION_DIR . '_placeholder' ) ) @mkdir ( BACKUP_ROTATION_DIR . '_placeholder', 0755 );
	if ( ! is_dir ( BACKUP_ROTATION_DIR . '_placeholder' ) ) closenow ( true, 'Backup rotation folder ( ' . BACKUP_ROTATION_DIR . ' ) is not write-able ...' );
	@touch ( BACKUP_ROTATION_DIR . 'test.txt' );
	if ( ! is_file ( BACKUP_ROTATION_DIR . 'test.txt' ) ) closenow ( true, 'Backup rotation folder ( ' . BACKUP_ROTATION_DIR . ' ) is not write-able ...' );
	@unlink ( BACKUP_ROTATION_DIR . 'test.txt' );
	
	// fase 2: ciclo di ricognizione del file tree della directory SORGENTE dei backup
	out ( ' ** Starting "' . BACKUP_DIR . '" folder accounts+files recognition...' );
	$files_found = 0;
	$accounts_found = 0;
	$backup_tree = Array ();
	$remember_backup_account_roots = Array ();
	foreach ( $backup_dir_contents as $i => $fullpath ) {
		$stripped_path = str_replace ( BACKUP_DIR, '', $fullpath );
		$chunks = explode ( '/', $stripped_path );
		if ( count ( $chunks ) == 1 ) {
			if ( ! array_key_exists ( $chunks[0], $backup_tree ) && $chunks[0] != EXCLUDE_BACKUP_FOLDER ) {
				$backup_tree[$chunks[0]] = Array ();
				$remember_backup_account_roots[] = $fullpath;
				$accounts_found++;
				continue;
			}
		}
		else if ( count ( $chunks ) == 2 ) {
			if ( ! array_key_exists ( $chunks[0], $backup_tree ) && $chunks[0] != EXCLUDE_BACKUP_FOLDER ) {
				$backup_tree[$chunks[0]] = Array ();
				$remember_backup_account_roots[] = $fullpath;
				$accounts_found++;
			}
			if ( is_file ( BACKUP_DIR . $chunks[0] . '/' . $chunks[1] ) && $chunks[1] != EXCLUDE_BACKUP_FILENAME ) {
				$mtime = filemtime ( BACKUP_DIR . $chunks[0] . '/' . $chunks[1] );
				$backup_tree[$chunks[0]][bdate($mtime)][] = BACKUP_DIR . $chunks[0] . '/' . $chunks[1];
				$files_found++;
			}
			else continue;
		}
		else if ( count ( $chunks ) == 3 ) {
			if ( ! array_key_exists ( $chunks[0], $backup_tree ) && $chunks[0] != EXCLUDE_BACKUP_FOLDER ) {
				$backup_tree[$chunks[0]] = Array ();
				$remember_backup_account_roots[] = $fullpath;
				$accounts_found++;
			}
			if ( is_file ( BACKUP_DIR . $chunks[0] . '/' . $chunks[1] . '/' . $chunks[2] ) && $chunks[2] != EXCLUDE_BACKUP_FILENAME ) {
				$mtime = filemtime ( BACKUP_DIR . $chunks[0] . '/' . $chunks[1] . '/' . $chunks[2] );
				$backup_tree[$chunks[0]][bdate($mtime)][] = BACKUP_DIR . $chunks[0] . '/' . $chunks[1] . '/' . $chunks[2];
				$files_found++;
			}
			else continue;
		}
	}
	out ( ' // Done working on "' . BACKUP_DIR . '": found ' . $accounts_found . ' eligible ACCOUNTS and ' . $files_found . ' eligible FILES to be copied into backup rotation directory' );
	
	// fase 3: per ogni elemento nell'array $backup_tree controllo nella directory dei backup rotation se ho quella data e quell'account "root" salvato
	out ( ' ** Starting "' . BACKUP_ROTATION_DIR . '" smart copy...' );
	foreach ( $backup_tree as $account_name => $date_to_filename ) {
		foreach ( $date_to_filename as $date_ymd => $filenames ) {
			foreach ( $filenames as $i => $filename ) {
				// prima di tutto, se non esiste ancora, creo la directory con la data come "livello zero"
				if ( ! is_dir ( BACKUP_ROTATION_DIR . $date_ymd ) ) {
					out ( ' --> Going to create a folder: ' . BACKUP_ROTATION_DIR . $date_ymd );
					@mkdir ( BACKUP_ROTATION_DIR . $date_ymd, 0755 );
				}
				if ( ! is_dir ( BACKUP_ROTATION_DIR . $date_ymd ) ) closenow ( true, 'Backup rotation folder ( ' . BACKUP_ROTATION_DIR . ' ) is not write-able ...' );
				
				// poi, se non esiste ancora, creo la directory dell'account
				if ( ! is_dir ( BACKUP_ROTATION_DIR . $date_ymd . '/' . $account_name ) ) {
					out ( ' --> Going to create a folder: ' . BACKUP_ROTATION_DIR . $date_ymd . '/' . $account_name );
					@mkdir ( BACKUP_ROTATION_DIR . $date_ymd . '/' . $account_name, 0755 );
				}
				if ( ! is_dir ( BACKUP_ROTATION_DIR . $date_ymd . '/' . $account_name ) ) closenow ( true, 'Backup rotation folder ( ' . BACKUP_ROTATION_DIR . ' ) is not write-able ...' );
				
				// in ultimo, se non esiste il file, lo copio
				if ( ! is_file ( BACKUP_ROTATION_DIR . $date_ymd . '/' . $account_name . '/' . $filename ) ) {
					out ( ' --> Going to copy a backup file: ' . $filename );
					$file_basename = pathinfo ( $filename, PATHINFO_BASENAME );
					if ( copy ( $filename, BACKUP_ROTATION_DIR . $date_ymd . '/' . $account_name . '/' . $file_basename ) ) {
						@unlink ( $filename );
					}
					else closenow ( true, 'Closing backup rotation now... the file copy of "' . $filename . '" to "' . BACKUP_ROTATION_DIR . $date_ymd . '/' . $account_name . '/' . $file_basename . '" failed with no apparent reason...' );
				}
			}
		}
	}
	
	// fase 4: creo l'albero delle cartelle/file sulla directory di rotazione backup
	out ( ' ** Starting "' . BACKUP_ROTATION_DIR . '" automated erase of backup files greater than ' . BACKUP_RETENTION_PERIOD . ' days old' );
	$backuprotation_dir_contents = Array ();
	$objects = new RecursiveIteratorIterator ( new RecursiveDirectoryIterator ( BACKUP_ROTATION_DIR ), RecursiveIteratorIterator::SELF_FIRST );
	if ( $objects ) {
		foreach ( $objects as $name => $object ){
			if ( $name !== '.' && $name !== '..' ) {
				$name = realpath ( $name );
				if ( ! in_array ( $name, $backuprotation_dir_contents ) && $name !== '/' && $name !== rtrim ( BACKUP_ROTATION_DIR, '/' ) ) $backuprotation_dir_contents[] = $name;
			}
		}
	}
	
	// fase 5: cancello tutti i file, sulla directory di backup, che sono più anziani della BACKUP_RETENTION_PERIOD ( ovvero, i backup troppo vecchi )
	$today_tstamp = strtotime ( date ( 'Y' ) . '-' . date ( 'm' ) . '-' . date ( 'd' ) . ' 00:00:00' );
	$backup_retention_deadline_tstamp = strtotime ( '-' . BACKUP_RETENTION_PERIOD . ' days', $today_tstamp );
	foreach ( $backuprotation_dir_contents as $i => $fullpath ) {
		// sicuramente il primo chunk è relativo alla data, per come è costruito l'albero di copia/incolla
		$stripped_path = str_replace ( BACKUP_ROTATION_DIR, '', $fullpath );
		$chunks = explode ( '/', $stripped_path );
		if ( count ( $chunks ) == 1 && $chunks[0] !== '_placeholder' ) {
			if ( preg_match ( '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/', $chunks[0], $match ) ) {
				$date_of_folder_tstamp = strtotime ( $chunks[0] . ' 00:00:00' );
				if ( $date_of_folder_tstamp < $backup_retention_deadline_tstamp ) {
					out ( ' --> folder ' . $chunks[0] . ' needs to be purged, it's more than ' . BACKUP_RETENTION_PERIOD . ' days old...' );
					emptyDirectory ( BACKUP_ROTATION_DIR . $chunks[0], true );
				}
				else out ( ' --> folder ' . $chunks[0] . ' can be left on place... it's not time for its death ... for now ...' );
			}
			else closenow ( true, 'Damnit... folder ' . $fullpath . ' failed the preg_match check on the first chunk ( ' . $chunks[0] . ' )' );
		}
	}
	
	// fase 5: per ogni "$remember_backup_account_roots" devo svuotare la cartella perchè è già stata processata
	out ( ' ** Starting "' . BACKUP_DIR . '" automated erase of backup files that have been already copied in this session...' );
	foreach ( $remember_backup_account_roots as $i => $folder ) {
		emptyDirectory ( $folder );
	}
	
	// fatto!
	out ( '' );
	out ( '    yayyy!! everything done!' );
	out ( '' );
	out ( '    Copyright (c) Maurizio Fonte 2016 - https://www.mauriziofonte.it' );
	closenow ();
	
	function bdate ( $tstamp ) {
		return date ( 'Y-m-d', $tstamp );
	}
	
	function emptyDirectory ( $directory, $remove_parent = false ) {
		$files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator ( $directory, RecursiveDirectoryIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST );
		if ( $files ) {
			foreach ( $files as $fileinfo ) {
				$todo = ( $fileinfo -> isDir ( ) ) ? 'rmdir' : 'unlink';
				$todo ( $fileinfo -> getRealPath ( ) );
			}
			if ( $remove_parent ) rmdir ( $directory );
		}
	}
	
	function closenow ( $error = false, $error_string = null ) {
		
		if ( ! is_dir ( HOME_DIR . LOGS_DIR ) ) mkdir ( HOME_DIR . LOGS_DIR, 0755 );
		if ( ! is_dir ( HOME_DIR . LOGS_DIR . date ( 'Y-m' ) . '/' ) ) mkdir ( HOME_DIR . LOGS_DIR . date ( 'Y-m' ) . '/', 0755 );
		
		if ( $error && ! empty ( $error_string ) ) {
			out ( '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' );
			out ( '!! program detected an error and is going to be killed NOW !!' );
			out ( '!!' );
			out ( '!! ERROR_REASON = ' . $error_string );
		}
		
		$out = ob_get_clean ();
		file_put_contents ( HOME_DIR . LOGS_DIR . date ( 'Y-m' ) . '/' . date ( 'Y-m-d-H-i-s' ) . '.txt', $out );
		exit ();
	}
	
	function out ( $string ) {
		echo $string . chr(10);
	}
?>

The code is pretty straightforward. If your backups fall every day into /BACKUPS/name_of_server, then you make this script "read" the contents of the /BACKUP/ directory. It will take care of moving the backups into "BACKUP_ROTATION_DIR" and will make sure that backup files that are older than "BACKUP_RETENTION_PERIOD" will get deleted at the right day.

To make it run correctly, place this script into its own directory, because it will automatically create a "logs" folder for you. If things go bad, go check the script verbose output into the right log file. Then, make it run via a new cronjob, and make sure that the user that actually runs the PHP script has the necessary permissions to read/write into the backup rotation folder.

Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2015, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Le Landing Page sono uno degli strumenti più efficaci per rendere redditizia una campagna pubblicitaria fatta su Internet, come ad esempio fatta su Google Adwords.

Ma prima di tutto, che cos'è una landing page? Vediamo di spiegarlo con poche parole, e in maniera efficace. Mentre leggi la guida intanto puoi aprire in una nuova scheda un esempio di landing page creata da Web Sunrise, una Web Agency a Torino che si occupa di sviluppo di Siti Internet e pubblicità su Google Adwords.

Facciamo finta che tu sia un produttore di occhiali da sole, e che tu costruisca un nuovo modello di occhiali che si chiama Occhiali da Sole ACME sul quale vuoi puntare la crescita della tuo fatturato aziendale. Hai già un sito internet di successo, ma per qualche ragione le visite alle pagine dedicate agli occhiali da sole ACME non vengono visualizzate abbastanza.
A questo punto, parlando con il vostro responsabile commerciale, decidete di fare una campagna pubblicitaria in internet per aumentare le visite al tuo sito, e in particolar modo di aumentare le visite ai nuovi e fantastici occhiali da sole appena prodotti.

Fatta questa decisione, rimane un problema. Le campagne di pubblicità in Internet hanno bisogno di:

  • 1. Una Pagina di Destinazione della campagna pubblicitaria, ovvero la pagina che visiteranno i clienti subito dopo aver "cliccato" sulla pubblicità
  • 2. Una o più Parole Chiave per la campagna di pubblicità ovvero le "parole" con le quali volete che i vostri clienti vi trovino
  • 3. Uno o più Annunci di poche parole, semplici ed essenziali, che verranno mostrati ai clienti su siti esterni ( ad esempio su Google in caso di campagne AdWords )

A questo punto capirai bene che il compito del responsabile commerciale non è semplice: potrebbe essere bravo a fare i punti 2 e 3 dell'esempio qui sopra, ma rimarrebbe comunque un nodo cardine, ovvero che cosa mostrare ai clienti nella pagina di destinazione. Questo fa veramente la differenza tra una campagna pubblicitaria che funziona e una che non funziona.

Ecco che cos'è una landing page: una pagina di destinazione che serve in qualche modo a "veicolare" le scelte dei tuoi clienti, e a massimizzare i profitti dati dall'investimento in pubblicità cercando di trasformare "ogni cliente che vede la pagina Landing Page" in un "cliente che compra i tuoi occhiali da sole"

Quindi, che cosa deve avere una Landing Page per essere appagante, e trasformare i potenziali clienti in reali clienti? Semplice. Deve essere costruita ad-hoc con un misto di grafica, messaggi semplici ed intuitivi, video, ed eventualmente sconti od offerte pensate appositamente.

Ecco un elenco delle cose che una landing page deve necessariamente seguire:

  • Deve essere compatibile con tutti i dispositivi ( computer, tablet e smartphone )
  • Deve avere molta grafica, e questa grafica deve essere carina e "lussuosa"
  • Deve contenere un messaggio chiaro e preciso del suo obiettivo: se stai vendendo occhiali da sole, devi farlo capire in meno di 1 secondo al cliente
  • Deve concludersi con una Conversione, che nel nostro esempio si traduce in "vendere occhiali subito" o almeno fare in modo che il cliente li compri nel prossimo futuro. In generale la "conversione" è una parola che identifica un raggiungimento di un obiettivo nel mondo reale, quindi il passaggio vero e proprio dal mondo di Internet ad una interazione reale ( preventivo, chiamata, vendita, appuntamento ).

Nella pagina di esempio di creazione landing page che abbiamo linkato sopra, noterai che tutti i postulati elencati sopra sono rispettati.

In generale, creare una landing page non è semplice nè dal punto di vista progettuale ( quindi, che cosa scriverci dentro o metterci dentro ), nè dal punto di vista realizzativo. Per realizzare, ad esempio, la landing page linkata qui sopra, servono circa 10 giorni lavorativi tra costruzione della pagina, raccolta dati, e successive ottimizzazioni. Non è, quindi, un lavoro che può eseguire chiunque. Come regola generale, è meglio evitare assolutamente chi offre servizi di costruzione gratuita di landing page perchè l'unico sistema per rendere efficace una landing page è solo quello di realizzarla a regola d'arte. Solo così si può avere un rendimento che superi di molto l'investimento iniziale per l'avvio della campagna pubblicitaria.

Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2013, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Forse non tutti sanno che il vecchio e glorioso megaupload è stato rimpiazzato, da già un pò di tempo, da un nuovo servizio chiamato MEGA ( mega.co.nz ) sempre sviluppato da grossa parte del team che a suo tempo aveva sviluppato megaupload e megavideo.

Una bella funzionalità di megaupload era la sua estrema semplicità nell'integrazione con PHP, sia in download che in upload.. Infatti, con qualche linea di codice PHP si poteva tranquillamente scaricare un file ( anche senza account premium ), o uploadarne uno.

Il nuovo servizio MEGA, a differenza del suo predecessore, utilizza un ragionamento diverso per quanto riguarda lo storage interno degli upload che la gente invia. Infatti, effettuare un download via PHP è leggermente più complesso, a causa degli algoritmi di crittazione legati ai dati binari del file da scaricare.

Qui vi viene in aiuto la classe mf_php_megaconz_downloader, acronimo di Maurizio Fonte Mega.co.nz Php Downloader. Questa classe vi permette, con un paio di righe di codice, di effettuare un download dai serveri di mega.co.nz direttamente via PHP. Vediamo quindi il codice sorgente.

CODICE SORGENTE DELLA CLASSE MF PHP MEGA.CO.NZ DOWNLOADER ( richiede un server LAMP con PHP >= 4, e estensione mcrypt attiva e abilitata )

<?php
/*
* File: class.mega.co.nz.php
* Author: Maurizio Fonte
* Copyright: 2013 Maurizio Fonte
* Date: 10/09/2013
* Link: http://www.mauriziofonte.it/blog/post/scaricare-file-mega-co-nz-con-php.html
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details:
* http://www.gnu.org/licenses/gpl.html
*
*/
	
	class mf_php_megaconz_downloader {
	
		private static $downloadErrors = Array ( 
													1 => 'decryption of the downloaded file was wrong ( CRC check failed )',
													2 => 'There was a problem in writing the downloaded file into your filesystem. Check permissions...',
													3 => 'couldn\'t get remote contents of the file from mega.co.nz',
													4 => 'no direct download url found... private file?',
													5 => 'couldn\'t ping mega.co.nz api url, or api istance params incorrect',
													6 => 'file key was wrong: check for typos in the mega.co.nz download url',
													7 => 'mega.co.nz download url was incorrect, couldn\'t start the download process...'
												);
													
		/* Public mega.co.nz download descriptor IDs */
		public $fileid = null;
		public $filekey = null;
		
		/* private download-related variables */
		private $seqno = null;
		private $sid = null;
		private $download_url_is_valid = false;
		
		/* public download-related variables */
		public $download_url = null;
		public $download_size = null;
		public $download_filename = null;
		public $download_attributes_object = null;
		public $download_path = null;
		
		/* public download error code and messages */
		public $download_error_code = null;
		public $download_error_message = null;
		
		
		public function mf_php_megaconz_downloader ( $url ) {
			
			if ( strpos ( $url, 'mega.co.nz' ) !== false ) {
				
				$url_parts = explode ( '!', end ( explode ( '#', $url ) ) );
				
				if ( is_array ( $url_parts ) && count ( array_keys ( $url_parts ) ) == 3 && empty ( $url_parts[0] ) && strlen ( $url_parts[1] ) > 0 && strlen ( $url_parts[2] ) > 0 ) {
					$this -> download_url_is_valid = true;
					$this -> seqno = rand ( 0, 0xFFFFFFFF );
					$this -> fileid = $url_parts[1];
					$this -> filekey = $url_parts[2];
				}
			}
			
		}
		
		public function download ( $destination_path = null, $destination_filename = null, $disable_crc_check = false ) {
			
			$download_error = null;
			
			if ( $this -> download_url_is_valid ) {
				
				$key = self::base64_to_a32 ( $this -> filekey );
				if ( is_array ( $key ) ) {
					
					$k = array ( $key[0] ^ $key[4], $key[1] ^ $key[5], $key[2] ^ $key[6], $key[3] ^ $key[7] );
					$iv = array_merge ( array_slice ( $key, 4, 2 ), array ( 0, 0 ) );
					$meta_mac = array_slice ( $key, 6, 2 );
					
					$file = $this -> api_req ( Array ( 'a' => 'g', 'g' => 1, 'p'=> $this -> fileid ) );
					if ( $file ) {
						$this -> download_url = $file -> g;
						$this -> download_size = $file -> s;
						$this -> download_attributes_object = self::dec_attr ( self::base64urldecode ( $file -> at ), $k );
						if ( $this -> download_attributes_object ) $this -> download_filename = $this -> download_attributes_object -> n;
						
						if ( $this -> download_url ) {
							
							$data_enc = self::curlpost ( $this -> download_url );
							if ( $data_enc !== false ) {
								
								$file_contents_decrypted = self::aes_ctr_decrypt ( $data_enc, self::a32_to_str ( $k ), self::a32_to_str ( $iv ) );
								
								$save_location = '';
								if ( ! empty ( $destination_path ) ) $save_location .= rtrim ( $destination_path, '/' ) . '/';
								if ( ! empty ( $destination_filename ) ) $save_location .= $destination_filename;
								else if ( ! empty ( $this -> download_filename ) ) $save_location .= $this -> download_filename;
								else $save_location .= '_downloaded_from_mega.co.nz_' . date ( 'Y-m-d.H.i.s' );
								
								if ( is_file ( $save_location ) ) unlink ( $save_location );
								touch ( $save_location );
								
								if ( ! $disable_crc_check ) {
									$file_mac = self::cbc_mac ( $file_contents_decrypted, $k, $iv );
									if ( array ( $file_mac[0] ^ $file_mac[1], $file_mac[2] ^ $file_mac[3] ) != $meta_mac ) $download_error = 1;
									else file_put_contents ( $save_location, $file_contents_decrypted );
								}
								else file_put_contents ( $save_location, $file_contents_decrypted );
								
								if ( ! is_file ( $save_location ) || ! filesize ( $save_location ) > 0 ) {
									@unlink ( $save_location );	// maybe the file was touched, but no content was written due to lack of space in the hdd
									$download_error = 2;
								}
							}
							else $download_error = 3;
							
						}
						else $download_error = 4;
						
					}
					else $download_error = 5;
					
				}
				else $download_error = 6;
				
			}
			else $download_error = 7;
			
			if ( $download_error !== null ) {
				$this -> download_error_code = $download_error;
				$this -> download_error_message = self::$downloadErrors[$download_error];
				return false;
			}
			
			$this -> download_path = $save_location;
			return $save_location;
		}
		
		public function getCleanFilename () {
			
			if ( ! empty ( $this -> download_filename ) && strlen ( $this -> download_filename ) > 0 ) {
				$clean = strtolower ( $this -> download_filename );
				$clean = preg_replace ( '/[^a-z0-9\.]/', '.', $clean );
				$clean = preg_replace ( '/\.\.+/', '.', $clean );
				return trim ( $clean, '.' );
			}
			
			return null;
			
		}
		
		private function api_req ( $req ) {
			$resp = self::curlpost ( 'https://g.api.mega.co.nz/cs?id=' .  ( $this -> seqno++ ) .  ( $this -> sid ? '&sid=' . $this -> sid : ''), json_encode ( array ( $req ) ) );
			$resp = json_decode ( $resp );
			return $resp[0];
		}
		
		private static function base64urldecode ( $data ) {
			$data .= substr ( '==', ( 2 - strlen ( $data ) * 3 ) % 4 );
			$data = str_replace ( array ( '-', '_', ',' ), array ( '+', '/', '' ), $data );
			return base64_decode ( $data );  
		}
		
		private static function str_to_a32 ( $b ) {
			$b = str_pad ( $b, 4 * ceil ( strlen ( $b ) / 4 ), '\0' );
			return array_values ( unpack ( 'N*', $b ) );
		}
		
		private static function a32_to_str ( $hex ) {
			return call_user_func_array ( 'pack', array_merge ( array ( 'N*' ), $hex ) );
		}

		private static function base64_to_a32 ( $s ) {
			return self::str_to_a32 ( self::base64urldecode ( $s ) );
		}
		
		private static function dec_attr ( $attr, $key ) {
			$attr = trim ( self::aes_cbc_decrypt ( $attr, self::a32_to_str ( $key ) ) );
			if ( substr ( $attr, 0, 6 ) != 'MEGA{"' ) return false;
			return json_decode ( substr ( $attr, 4 ) );
		}
		
		private static function aes_cbc_decrypt ( $data, $key ) {
			return @mcrypt_decrypt ( MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_CBC, '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0' );
		}
		
		private static function aes_cbc_encrypt ( $data, $key ) {
			return @mcrypt_encrypt ( MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_CBC, '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0' );
		}
		
		private static function aes_ctr_decrypt ( $data, $key, $iv ) {
			return @mcrypt_decrypt ( MCRYPT_RIJNDAEL_128, $key, $data, 'ctr', $iv );
		}
		
		private static function aes_cbc_encrypt_a32 ( $data, $key ) {
			return self::str_to_a32 ( self::aes_cbc_encrypt ( self::a32_to_str ( $data ), self::a32_to_str ( $key ) ) );
		}
		
		private static function cbc_mac ( $data, $k, $n ) {
			$padding_size = ( strlen ( $data ) % 16 ) == 0 ? 0 : 16 - strlen ( $data ) % 16;
			$data .= str_repeat ( '\0', $padding_size );

			$chunks = self::get_chunks ( strlen ( $data ) );
			$file_mac = array ( 0, 0, 0, 0 );

			foreach ( $chunks as $pos => $size ) {
				$chunk_mac = array ( $n[0], $n[1], $n[0], $n[1] );
				for ( $i = $pos; $i < $pos + $size; $i += 16 ) {
				  $block = self::str_to_a32 ( substr ( $data, $i, 16 ) );
				  $chunk_mac = array ( $chunk_mac[0] ^ $block[0], $chunk_mac[1] ^ $block[1], $chunk_mac[2] ^ $block[2], $chunk_mac[3] ^ $block[3] );
				  $chunk_mac = self::aes_cbc_encrypt_a32 ( $chunk_mac, $k );
				}
				$file_mac = array($file_mac[0] ^ $chunk_mac[0], $file_mac[1] ^ $chunk_mac[1], $file_mac[2] ^ $chunk_mac[2], $file_mac[3] ^ $chunk_mac[3]);
				$file_mac = self::aes_cbc_encrypt_a32 ( $file_mac, $k );
			}

			return $file_mac;
		}
		
		private static function get_chunks ( $size ) {
			$chunks = array();
			$p = $pp = 0;

			for ( $i = 1; $i <= 8 && $p < $size - $i * 0x20000; $i++ ) {
				$chunks[$p] = $i * 0x20000;
				$pp = $p;
				$p += $chunks[$p];
			}

			while ( $p < $size ) {
				$chunks[$p] = 0x100000;
				$pp = $p;
				$p += $chunks[$p];
			}

			$chunks[$pp] = ($size - $pp);
			if ( ! $chunks[$pp] ) unset ( $chunks[$pp] );

			return $chunks;
		}

		private static function curlpost ( $url, $data = null ) {
			$ch = curl_init ( $url );
			curl_setopt ( $ch, CURLOPT_RETURNTRANSFER, true );
			if ( ! empty ( $data ) ) {
				curl_setopt ( $ch, CURLOPT_POST, true );
				curl_setopt ( $ch, CURLOPT_POSTFIELDS, $data );
			}
			$resp = curl_exec ( $ch );
			curl_close ( $ch );
			
			if ( ! empty ( $resp ) ) return $resp;
			return false;
		}
		
	}
?>

UTILIZZO DELLA CLASSE ( esempio applicativo )

<?php
	require_once 'class.megaconz.php';									// la classe MF Php Mega.co.nz Downloader
	
	$temp_filename = md5 ( microtime () . rand ( 111111, 999999 ) );	// un nome file a caso
	$working_directory = '/directory/where/to/save/the/file/';			// la directory dove verra' salvato il file scaricato ( il nome del file e' stato calcolato sopra )
	$md = new mf_php_megaconz_downloader ( '##URL-MEGACONZ##' );		// istanza della classe per il download, passando l'url del file da scaricare
	if ( ( $saved_filename = $md -> download ( $working_directory, $temp_filename, true ) ) !== false && ( $save_as = $md -> getCleanFilename () ) !== false ) {
		// se il download e' corretto ( esiste, quindi, il file "$saved_filename", lo salviamo sempre nella directory "$working_directory", ma chiamandolo con il nome REALE del file su mega.co.nz con il metodo $md -> getCleanFilename ()
		rename ( $saved_filename, $working_directory . $save_as );
	}
	else echo $md -> download_error_message;	// stampiamo l'errore se qualcosa e' andato storto
	
	// DEBUG: stampiamo il contenuto della istanza della classe mf_php_megaconz_downloader
	print_r ( $md );
?>

Come avete potuto notare, con pochissime righe si può scaricare un file da mega.co.nz e salvarlo in locale, semplicemente partendo da una url pubblica di download. Ovviamente, la classe funziona solo e soltanto con i download pubblici, ovvero quelli che non sono stati resi "privati" dall'uploader originale di quel file.

Nota importante: Qualsiasi download da un servizio di hosting di file è "adattabile" ad una piccola classe di interfaccia per il download. Basta cercare in internet, o farmi un fischio tramite il modulo contattami per richiedermi una analisi :)

Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2011, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Un buon sito si costruisce prima di tutto creandolo con funzionalità multilingua. Ovvero, sulla base dell'IP dell'utente, cercare di mandare in output il sito nel suo linguaggio senza troppi fronzoli tecnici.

Questo si traduce, nella pratica, nella creazione di un sito con una buona classe di traduzione delle stringhe on-the-fly, un pò come fa wordpress. Per esempio, wordpress fa uso dei file di traduzione *.po e *.mo.. Si definisce una stringa, con la sua "chiave primaria" nel linguaggio che si vuole, ad esempio:

<div id="test"><?php echo __ ( 'This is the main key for the string "this is the main key for the string"' ); ?></div>

A questo punto, aiutandovi con le funzioni di output buffering, e sulla base della geolocation dell'IP dell'utente che sta visualizzando il sito, potete mandare in output la traduzione corretta della stringa This is the main key for the string "this is the main key for the string"

In internet esistono svariate classi che permettono la geolocation. A me personalmente non piace fare le cose semplici, e mi piace avere la situazione sotto controllo. Quindi, mi sono documentato, e ho trovato un sito che permette il download di un file di mapping tra indirizzi IP e stati.

Avendo in mano un file del genere, è possibile creare un DB che abbia le referenziazioni tra IP e location, e così possiamo dare sfogo alla nostra creatività in termini di output da mandare all'utente ^^

Prima di tutto, prepariamo i DB che contengono i dati del DB IP-to-country

CREATE TABLE IF NOT EXISTS `ip_to_country` (
  `id` int(11) NOT NULL auto_increment,
  `ip_start_integer` int(11) NOT NULL,
  `ip_end_integer` int(11) NOT NULL,
  `country_id` int(11) NOT NULL,
  PRIMARY KEY  (`id`),
  UNIQUE KEY `ip_start_integer_index` (`ip_start_integer`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1;

CREATE TABLE IF NOT EXISTS `country_list` (
  `id` int(11) NOT NULL auto_increment,
  `country_code` varchar(10) character set utf8 collate utf8_bin NOT NULL,
  `country_name_en` varchar(255) character set utf8 collate utf8_bin NOT NULL,
  `country_name_it` varchar(255) character set utf8 collate utf8_bin NOT NULL,
  PRIMARY KEY  (`id`),
  UNIQUE KEY `country-name` (`country_name_en`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1;

Adesso, passiamo alla parte divertente, cioè al parsing del file di mapping IP-country e al relativo salvataggio dei dati nelle nostre 2 tabelle mysql

<?php
	/*
		http://www.ipinfodb.com/
		
		API KEY: bisogna registrarsi per averla
		http://lite.ip2location.com/?file=IP2LOCATION-LITE-DB1.CSV.ZIP&key=	
	*/

	// CONNESSIONE AL DB - DA INSERIRE QUI
	
	$stream_ZIPFILE = __CURL_getURL( 'http://lite.ip2location.com/?file=IP2LOCATION-LITE-DB1.CSV.ZIP&key=la-chiave-api-del-sito-ipinfodb.com', 'http://www.ipinfodb.com/download.php?file=IP2LOCATION-LITE-DB1.CSV.ZIP' );
	file_put_contents ( 'tempzipfile', $stream_ZIPFILE );
	$stream_ZIPFILE = ''; // releasing memory for this variable
	
	$zip = zip_open ( 'tempzipfile' );
	unlink ( 'tempzipfile' );
	do {
		$entry = zip_read($zip);
	} while ($entry && zip_entry_name($entry) != "IP2LOCATION-LITE-DB1.CSV");
	
	zip_entry_open($zip, $entry, "r");
	$entry_content = zip_entry_read($entry, zip_entry_filesize($entry));
	$entry_content = str_replace ( chr(13).chr(10), chr(10), $entry_content );
	
	$linee = explode ( chr(10), $entry_content );
	$entry_content = ''; // releasing memory for entry_content ( it's a long string )
	
	// flushing ip_to_country DB
	$sql = "TRUNCATE TABLE ip_to_country";
	mysql_query($sql);
	
	// parsing Text DB Lines
	foreach ( $linee as $linea ) {
		list ( $start_ip_integer, $end_ip_integer, $country_code, $country_name ) = explode ( ',', str_replace ( '"', '' , $linea ) );
		
		// inserting country into country DB  (if needed)
		$sql = "SELECT id FROM country_list WHERE country_name_en='" . $country_name . "'";
		$rs = mysql_query ( $sql );
		if ( $rs && mysql_num_rows ( $rs ) == 1 ) $idCountry = mysql_result ( $rs, 0 );
		else {
			// translating country in Italian
			$stream_TRANSLATE = __CURL_getURL( 'https://www.googleapis.com/language/translate/v2?key=la-tua-api-key-google.translate&q=' . urlencode ( $country_name ) . '&source=en&target=it', 'http://www.google.it/' );
			$stream_TRANSLATE_array = json_decode ( $stream_TRANSLATE );
			$countryname_IT = strtoupper ( $stream_TRANSLATE_array -> data -> translations[0] -> translatedText );
			
			$sql = "INSERT INTO country_list ( country_code, country_name_en, country_name_it ) VALUES ( '$country_code', '$country_name', '$countryname_IT' )";
			mysql_query ( $sql );
			
			$idCountry = mysql_insert_id();
		}
		
		$sql = "INSERT INTO ip_to_country ( ip_start_integer, ip_end_integer, country_id ) VALUES ( '$start_ip_integer', '$end_ip_integer', '$idCountry' )";
		mysql_query ( $sql );
	}


	function __CURL_getURL($url, $referer) {
		$header[0] = "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
		$header[] = "Cache-Control: max-age=0";
		$header[] = "Connection: keep-alive";
		$header[] = "Keep-Alive: 300";
		$header[] = "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7";
		$header[] = "Accept-Language: it-it,it;q=0.8,en-us;q=0.5,en;q=0.3";
		$header[] = "Pragma: ";
		$header[] = "Referer: " . $referer;

		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
		curl_setopt($ch, CURLOPT_HTTPHEADER, $header); 
		curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3");
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_VERBOSE, true);
		curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
		curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
		$result = curl_exec($ch);
		curl_close($ch);

		return $result;
	}

?>

Per avviare questo codice, avrete bisogno di 2 api key:

  • Una api key del servizio http://www.ipinfodb.com/ - rimpiazza la stringa "la-chiave-api-del-sito-ipinfodb.com" all'interno del sorgente qui sopra con la tua api key
  • Una api key del servizio Google Translate API - rimpiazza la stringa "la-tua-api-key-google.translate" all'interno del sorgente qui sopra con la tua api key

Una volta fatto tutto, potete schedulare questo script per l'esecuzione automatica con un cronjob. Anche una volta alla settimana va bene, non è necessario farlo partire ogni ora ^^

Ora, ti starai chiedendo: e come faccio a stabilire la geolocation dell'utente con questi due database? Bene, ecco le poche linee di codice per sfruttare le tabelle.

// ottenere la "country_id" dal database delle country per un particolare IP
$ip = $_SERVER['REMOTE_ADDR']; // o qualsiasi ip nel formato 123.123.123.123 ottenuto con qualsiasi metodo
$sql = "SELECT country_id FROM ip_to_country WHERE ip_start_integer <= " . ip2long ( $ip ) . " ORDER BY ip_start_integer DESC LIMIT 1";
$rs = mysql_query ( $sql );
if ( $rs ) $country_id = mysql_result ( $rs, 0 );

Con la country_id per un particolare IP, sarà vostro compito stabilire quale lingua mandare in output all'utente.
Ovviamente, questo database può avere tantissimi risvolti pratici. Un altro che mi viene in mente è la geolocalizzazione di server proxy, o la geolocalizzazione degli IP di una applicazione di tracciamento degli utenti.

Insomma, con le tabelle IP-to-country avrete a disposizione tantissime possibilità di sviluppo e soprattutto, avendo in mano i dati nel vostro DB, potrete generare i volumi di traffico che desiderate per quanto riguarda le geolocation calcolate per ora. Infatti, alcuni servizi online di geolocalizzazione, vi impediscono di fare più di un tot di query all'ora.

Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2011, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Se hai un sito qualsiasi, dove richiedi la registrazione per usufruire del sito, molto probabilmente non hai mai pensato che le email che gli utenti utilizzano per registrarsi possano diventare denaro..

L'argomento che andrò a trattare oggi è un pò scottante, lo ammetto: le email di spam, pubblicitarie, sono davvero fastidiose. Ma se abbiamo un sito, e stiamo richiedendo la verifica dell'email dopo la registrazione, quelle email valgono oro.

Il problema, però è il seguente: nessun advertiser accetterà mai delle liste di email nelle quali l'utente non abbia specificatamente dichiarato di voler ricevere delle email pubblicitarie.

Quindi, come facciamo a trasformare un semplice e scarno database di contatti email in un vero e proprio database di email DOUBLE OPT-IN?

Beh, lasciamo fare il lavoro sporco a PHP+mySQL. Per prima cosa, creiamo le tabelle del database per eseguire gli script che faranno funzionare il gioco:

CREATE TABLE IF NOT EXISTS `email_marketing_contacts` (
  `id` int(11) NOT NULL auto_increment,
  `email` varchar(255) character set utf8 collate utf8_bin NOT NULL,
  `subscribed` tinyint(1) NOT NULL default '0',
  `unsubscribed` tinyint(1) NOT NULL default '0',
  `info_emails` tinyint(1) NOT NULL default '0',
  `double_opt_in` tinyint(1) NOT NULL default '0',
  PRIMARY KEY  (`id`),
  KEY `index-emails` (`email`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1;

CREATE TABLE IF NOT EXISTS `email_marketing_lists` (
  `id` int(11) NOT NULL auto_increment,
  `email_subject` varchar(255) NOT NULL,
  `email_content_text` longtext NOT NULL,
  `email_content_html` longtext NOT NULL,
  `creation_date` datetime NOT NULL,
  `active` tinyint(1) NOT NULL default '1',
  `completion_date` datetime NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1;

CREATE TABLE IF NOT EXISTS `email_marketing_emailstocrons` (
  `id` int(11) NOT NULL auto_increment,
  `listid` int(11) NOT NULL,
  `contactid` int(11) NOT NULL,
  `email` varchar(255) character set utf8 collate utf8_bin NOT NULL,
  `sent` tinyint(1) NOT NULL default '0',
  `sent_date` datetime NOT NULL,
  `status` tinyint(1) NOT NULL default '0',
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1;

Ovviamente, all'inizio, tutti i contatti dovranno essere inseriti dentro il database "email_marketing_contacts". Sarà vostro compito creare uno script che tenga aggiornato il database dei contatti, senza sovrascritture delle email o compagnia bella.

A questo punto, diamo un'occhiata agli script PHP per creare la nostra bella lista di email double opt-in. Servono:

  • Uno script per la creazione di "newsletter" da mandare a tutti i contatti di "email_marketing_contacts"
  • Uno script ( magari cronnato ) che invii le email, magari a scaglioni di 100 per volta, per evitare che i servizi di web hosting economici rompano le scatole se inviamo troppe email per volta
  • Una pagina cliccabile nell'email che arriverà agli utenti, nella quale l'utente possa scegliere se iscriversi double opt-in oppure disiscriversi completamente

Codice 1: Creazione delle Email

<?php
	if ( isset ( $_POST['email_subject'] ) && strlen ( $_POST['email_subject'] ) > 0 ) {
		
		// CONNESSIONE AL DB - INSERIRE QUI
		
		$sql = "INSERT INTO email_marketing_lists ( email_subject, email_content_text, email_content_html, creation_date ) VALUES ( '" . mysql_real_escape_string ( $_POST['email_subject'] ) . "', '" . mysql_real_escape_string ( $_POST['email_content_text'] ) . "', '" . mysql_real_escape_string ( $_POST['email_content_html'] ) . "', '" . date ( 'Y-m-d H:i:s' ) . "' )";
		mysql_query ( $sql );
		
		if ( strlen ( mysql_error() ) == 0 ) {
			$listID = mysql_insert_id();
			
			$sql = "SELECT id, email FROM email_marketing_contacts ORDER BY id ASC";
			$rs = mysql_query ( $sql );
			while ( $rw = mysql_fetch_assoc ( $rs ) ) {
				$sql = "INSERT INTO email_marketing_emailstocrons ( listid, contactid, email ) VALUES ( '$listID', '" . $rw['id'] . "', '" . $rw['email'] . "' )";
				mysql_query ( $sql );
				$contatti_a_cron++;
			}
		}
	}
	
	if ( $contatti_a_cron > 0 ) echo '<p>Lista creata con successo! Aggiunti ' . $contatti_a_cron . ' contatti alla lista</p>';
?>

<form method="post">
	Oggetto Email: <input type="text" name="email_subject" /><br />
	Parte Testuale Email: <br />
<txtarea name="email_content_text" style="width: 1000px; height: 200px;"></txtarea><br />
	Parte HTML Email: <br />
<txtarea name="email_content_html" style="width: 1000px; height: 200px;"></txtarea><br />
<input type="submit" value="Invia" />
</form>

Codice 2: Invio delle Email Effettivo

<?php
	// CONNESSIONE AL DB - INSERIRE QUI
	
	$sql = "SELECT * FROM email_marketing_lists WHERE active='1' LIMIT 1";
	$rs = mysql_query ( $sql );
	if ( $rs && mysql_num_rows ( $rs ) == 1 ) {
		// esiste una lista attiva, vediamo che succede...
		$rw = mysql_fetch_assoc ( $rs );
		
		$listID = $rw['id'];
		$subject = stripslashes ( $rw['email_subject'] );
		$emailTextPart = wordwrap ( stripslashes ( $rw['email_content_text'] ), 50 );
		$emailHtmlPart = wordwrap ( stripslashes ( $rw['email_content_html'] ), 50 );
		
		$sql = "SELECT * FROM email_marketing_emailstocrons WHERE sent='0' AND listid='$listID' LIMIT 100";
		$rs2 = mysql_query ( $sql );
		if ( $rs2 && mysql_num_rows ( $rs2 ) >= 1 ) {
			// invio effettivo delle email...
			
			while ( $rw2 = mysql_fetch_assoc ( $rs2 ) ) {
				// prima di tutto, rimpiazzo la variabile di UTENTE con una stringa calcolata adesso
				$idUtente = base_convert( $rw2['contactid'] + 12445, 10, 36 );
				
				// Invio effettivo della email con il metodo doppio
				$random_hash = md5(microtime());
				$header = "From: Quello che vuoi <quellochevuoi@ciaociao.it>\n";
				$header .= "X-Mailer: PHP-simple-email-boundary\n";
				$boundary = "==String_Boundary_x" . md5(time()). "x";
				$header .= "MIME-Version: 1.0\n";
				$header .= "Content-Type: multipart/alternative;\n";
				$header .= " boundary=\"$boundary\";\n\n";
				
				$messaggio = "Se visualizzi questo testo, manda una email ad quellochevuoi@ciaociao.it specificando che non e' stato possibile leggere questa email \n\n";
				$messaggio .= "--$boundary\n";
				$messaggio .= "Content-Type: text/plain; charset=\"iso-8859-1\"\n";
				$messaggio .= "Content-Transfer-Encoding: 7bit\n\n";
				$messaggio .= str_replace ( '$$ID_UTENTE$$', $idUtente, $emailTextPart ) . "\n\n";
				$messaggio .= "--$boundary\n";
				$messaggio .= "Content-Type: text/html; charset=\"iso-8859-1\"\n";
				$messaggio .= "Content-Transfer-Encoding: 7bit\n\n";
				$messaggio .= str_replace ( '$$ID_UTENTE$$', $idUtente, $emailHtmlPart ) . "\n";
				
				$status = mail( $rw2['email'], $subject, $messaggio, $header );
				
				sleep ( 8 ); // mandiamone 1 ogni 8 secondi...
				
				$sql = "UPDATE email_marketing_emailstocrons SET sent='1', sent_date='" . date ( 'Y-m-d H:i:s' ) . "', status='$status' WHERE id='" . $rw2['id'] . "'";
				mysql_query ( $sql );
			}
		}
		else {
			// questa lista è stata completata! Quindi aggiorniamo la variabile "active" e settiamo la completion della lista ad adesso
			$sql = "UPDATE email_marketing_lists SET active='0', completion_date='" . date ( 'Y-m-d H:i:s' ) . "' WHERE id='$listID'";
			mysql_query ( $sql );
		}
	}
?>

Codice 3: Pagina di Ponte da linkare nell'email, per fare in modo che l'utente faccia la sua scelta

<html>
<head>
<title>pagina di ponte</title>
<style type="text/css">
	* { font-family: "Lucida Sans","Lucida Sans Regular","Lucida Grande","Lucida Sans Unicode",Geneva,Verdana,sans-serif; margin: 0; outline: 0 none; padding: 0; }
	body { background: none no-repeat scroll 0 0 #555555; font-size: 80%; }
	div#wrapper { background: none no-repeat scroll 0 0 #FFFFFF; border-left: 1px solid #888888; border-right: 1px solid #888888; box-shadow: 0 0 15px 5px #FFFFEE; margin: 0 auto; padding: 0 15px; position: relative; width: 650px; }
	div#header { height: 90px; position: relative; }
	div#header div#logo { height: 90px; left: 0; position: absolute; top: 0; width: 500px; }
	div#header div#logo a { background: url() no-repeat scroll 0 0 transparent; display: block; height: 90px; width: 315px; }
	div#header div#logo h2 { color: #7CA431; font-size: 11px; left: 65px; padding: 1px 3px; position: absolute; top: 5px; }
	div#header div#logo p { bottom: 0; color: transparent; font-size: 1px; height: 1px; left: -9000px; margin: 0; padding: 0; position: absolute; }
	h1 strong, h1 strong, h3 strong { left: -9000px; position: absolute; text-indent: -9000px; top: 0; }
	div#content { margin-top: 20px; }
	h3 { margin: 5px 0; color: #007BB4; }
	div#module { margin: 20px 0; border-top: 1px solid #888888; border-bottom: 1px solid #888888; background-color: #FFEEEE; padding: 30px; }
	
	form { text-align: center; margin-top: 15px; }
	form p { text-align: left; }
	input.submit { border: 1px solid #007BB4; font-weight: bold; font-size: 16px; padding: 5px; background-color: #FFF; text-align: left; }
	input.submit:hover { border: 1px solid #7CA431; background-color: #EEFFFF; }
</style>

<script type="text/javascript">
	function checkCheckboxes () {
		
		if ( ! document.getElementById ( 'infemails' ).checked && ! document.getElementById ( 'advemails' ).checked ) {
			if ( ! document.getElementById ( 'infemails' ).checked ) {
				var el = document.getElementById ( 'infemails' ).parentNode;
				el.style.backgroundColor = '#FCAC98';
				el.style.padding = '10px';
			}
			if ( ! document.getElementById ( 'advemails' ).checked ) {
				var el = document.getElementById ( 'advemails' ).parentNode;
				el.style.backgroundColor = '#FCAC98';
				el.style.padding = '10px';
			}
			var go = confirm ( 'Per iscriverti alla newsletter, e\' necessario mettere la spunta sia su "email informative" sia su "email pubblicitarie".\nVuoi continuare lo stesso con questa scelta? Cosi\' facendo non sarai iscritto alla newsletter,\ne non potrai rimanere aggiornato sulle novita\'.' );
			
			if ( go ) return true;
			else return false;
		}
		else return true;
	}
</script>
</head>
<body>
<div id="wrapper">
	<div id="header">
		<div id="logo">
			logo & header
		</div>
	</div>
<?php
	if ( isset ( $_POST['action'] ) && strlen ( $_POST['action'] ) > 0 ) {
		
		// CONNESSIONE AL DB QUI
		
		$id_utente = $_POST['id_utente'];
		
		// gestione POST
		switch ( $_POST['action'] ) {
			case 'sub':
				// sottoscrizione
				
				$infemails = ( isset ( $_POST['infemails'] ) && $_POST['infemails'] == 1 ) ? 1 : 0;
				$advemails = ( isset ( $_POST['advemails'] ) && $_POST['advemails'] == 1 ) ? 1 : 0;
				
				$sql = "SELECT * FROM email_marketing_contacts WHERE id='$id_utente'";
				$rs = mysql_query ( $sql );
				if ( $rs && mysql_num_rows ( $rs ) == 1 ) {
					$sql = "UPDATE email_marketing_contacts SET subscribed='1', unsubscribed='0', info_emails='$infemails', double_opt_in='$advemails' WHERE id='$id_utente'";
					mysql_query ( $sql );
				}
				break;
			case 'unsub':
				// disiscrizione
				$sql = "SELECT * FROM email_marketing_contacts WHERE id='$id_utente'";
				$rs = mysql_query ( $sql );
				if ( $rs && mysql_num_rows ( $rs ) == 1 ) {
					$sql = "UPDATE email_marketing_contacts SET unsubscribed='1', subscribed='0', info_emails='0', double_opt_in='0' WHERE id='$id_utente'";
					mysql_query ( $sql );
				}
				break;
			default: break;
		}
	}
	else if ( isset ( $_GET['u'] ) && strlen ( $_GET['u'] ) > 0 ) {
		$idUtente = ( base_convert( $_GET['u'], 36, 10 ) - 12445 );
?>
	<div id="content">
		 contenuto della email
	</div>
	<div id="module">
		<h2>Modulo di iscrizione/disiscrizione alla Newsletter</h2>
		<form method="post">
			<input type="hidden" name="action" value="unsub" />
			<input type="hidden" name="id_utente" value="<?php echo $idUtente; ?>" />
			<input type="submit" class="submit" value="Disiscrivimi per sempre dalla lista" />
		</form>
		<h3 style="margin-top 15px; text-align: center;">... oppure ...</h3>
		<form method="post" onsubmit="return checkCheckboxes();">
			<p style="text-align: left; font-size: 14px; margin: 5px 0;">Mantienimi iscritto alle email informative: <input type="checkbox" name="infemails" id="infemails" value="1" /> <strong>*</strong></p>
			<p style="text-align: left; font-size: 14px; margin: 5px 0;">Mantienimi iscritto alle email pubblicitarie: <input type="checkbox" name="advemails" id="advemails" value="1" /> <strong>*</strong></p>
			<input type="hidden" name="id_utente" value="<?php echo $idUtente; ?>" />
			<input type="hidden" name="action" value="sub" />
			<input type="submit" class="submit" value="Rimani iscritto alla newsletter" />
		<form>
		<p> </p>
		<p><strong>N.B.</strong></p>
		<p>Per <strong>rimanere iscritti alla newsletter</strong>, è necessario rimanere iscritti <strong>sia alle email informative, sia alle email pubblicitarie.</strong></p>
	</div>
<?php
	}
	else echo 'error';
?>
</div>
</body>
</html>

Capito tutto? In pratica, creiamo delle email con lo script 1. Le email saranno inviate in doppia modalità testuale+html, per rendere le email un pò più professionali. Questo significa che dovrete creare un testo e un sorgente html per la vostra email. Quindi, darete in pasto questi 2 sorgenti a questo script, che creerà un oggetto "email_marketing_lists" con oggetto, parte html e parte testo del vostro messaggio.

Una volta creata questa riga nel DB, non vi resterà che settare con un cronjob lo script di invio delle email ( il mio consiglio è di settare il cronjob per eseguire lo script ogni ora ).

Poi, tutto il resto lo farà la pagina di settaggio delle opzioni delle email degli utentu (script 3). Sarà vostro compito inserire, all'interno dell'email ( sia in formato testo che html ) un link alla pagina di modifica opzioni in questo formato:

http://www.ilmiodominio.it/lamiacartella/unaltra/index.php?u=$$ID_UTENTE$$

Lo script di invio delle email rimpiazzerà automaticamente questo placeholder con l'id effettivo del DB dei contatti di quella email. Il risultato? L'utente andrà a cliccare su un link che lo porterà ad una pagina che è già impostata sul suo id, quindi modificherà i dati suoi.

Avete visto quant'è semplice creare una lista di email double opt-in partendo da una lista che non è stata pensata per questo? Qualsiasi email si può trasformare in un potenziale introito di denaro extra con le email pubblicitarie!

DOM Tree parsing con PHP: phpQuery

Postato in Programmazione PHP
Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2009, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Quanti di voi non conoscono la libreria javascript jQuery? Penso pochi, se non nessuno. La potenza espressiva di questa libreria javascript permette una vastità quasi infinita di metodi di accesso agli elementi del DOM. La parte più interessante di questa libreria è senza dubbio la potenza del motore di selezione degli elementi, il Sizzle CSS Selector Engine.

Ora, quanti di voi programmatori PHP si saranno chiesti se questa potenza dei selettori sia applicabile anche a script PHP server-side che fungono da data crawler o data mining?
Penso parecchi.

Navigando in internet, ovviamente, la soluzione è già stata trovata ed il suo nome+link è PHPQuery Ecco un rapidissimo esempio di quello che questa classe può offrire: scarichiamo la index di google e troviamo i suoi link nel footer (Pubblicità, Soluzioni Aziendali, Tutto su Google, Google.com in English)

<?php
    require_once "class.phpquery.php";
    $document = phpQuery::newDocumentHTML( file_get_contents('http://www.google.it') );
    phpQuery::selectDocument($document);
    foreach ( pq ( 'span#footer a' ) as $link_footer_google ) {
        if ( pq( $link_footer_google ) -> html() != 'Privacy' ) {
            echo 'Link trovato nel footer della homepage di Google. Href=' . pq( $link_footer_google ) -> attr('href') . '; Text=' . pq( $link_footer_google ) -> html() . chr(10);
        }
    }
?>

Non mi dinlugherò in altri esempi più complessi per mostrarvi la potenza della classe: la pagina ufficiale di PHPquery offre già un ottimo manuale con le spiegazioni dei "metodi" sfruttabili.

Buon Lavoro!

Sicurezza e Validazione in PHP: prevenire gli attacchi

Postato in Programmazione PHP
Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2009, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Il problema della sicurezza è cruciale quando si sviluppa una applicazione in PHP. Se non vengono prese determinate precauzioni, un semplice script php come questo che sto per mostrarvi (una risposta xml, magari chiamata tramite ajax) può trasformarsi in un pericoloso buco per il vostro sito, e rendere possibile più o meno tutto a chi sta tentando di "lavorare" sopra lo script:

<?php
    // Connessione al db e roba varia
    $sql = "SELECT * FROM tabella WHERE id='" . $_GET['id_passato_da_get'] . "'";
    $rs = mysql_query($sql);
    $rw = mysql_fetch_assoc($rs);
    header('Content-Type:text/xml');
    echo '<?xml version="1.0" encoding="ISO-8859-1"?><root><dati>' . $rw['prova'] .'</dati></root>';
    exit();
?>

I problemi? Pessima programmazione: nessuna validazione dei dati, nessun controllo sul funzionamento della query, e in generale mancanza di occhio critico riguardo la sicurezza di uno script simile. Sarebbe aperto ad ogni tipologia di attacco.

Allora, come ovviamo a questo problema? In primis, programmando meglio. In secundis, facendoci aiutare da qualche classe che pulirà un pò i dati che vengono passati tramite GET e POST e che controlli un pò le impostazioni di sicurezza dell'environment (vedere register_globals per farsi un'idea)

Classe "Security PHP Class"

<?php
/**
* Provides static functions to help protect against cross site scripting
* attacks and helps clean up the php environment upon initializing.
*
* Based upon Security library by http://kohanaphp.com/
*
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*/

class Security
{
    // Instance of the security class.
    protected static $instance;
    protected $magic_quotes_gpc = FALSE;
    
    /**
     * Gets the instance of the Security class.
     *
     * @return object Instance of Security
     */
    public static function instance()
    {
        if(self::$instance === NULL)
        {
            return new Security;
        }
        
        return self::$instance;
    }
    
    
    /**
     * Constructor. Sanitizes global data GET, POST and COOKIE data.
     * Also makes sure those pesty magic quotes and register globals
     * don't bother us. This is protected because it really only needs
     * to be run once.
     *
     * @return void
     */
    protected function __construct()
    {
        if(self::$instance === NULL)
        {
            // Check for magic quotes
            if(get_magic_quotes_runtime())
            {
                // Dear lord!! This is bad and deprected. Sort it out ;)
                set_magic_quotes_runtime(0);
            }
            
            if(get_magic_quotes_gpc())
            {
                // This is also bad and deprected. See http://php.net/magic_quotes for more information.
                $this->magic_quotes_gpc = TRUE;
            }
            
            // Check for register globals and prevent security issues from arising.
            if(ini_get('register_globals'))
            {
                if(isset($_REQUEST['GLOBALS']))
                {
                    // No no no.. just kill the script here and now
                    exit('Illegal attack on global variable.');
                }
                
                // Get rid of REQUEST
                $_REQUEST = array();
                
                // The following globals are standard and shouldn't really be removed
                $preserve = array('GLOBALS', '_REQUEST', '_GET', '_POST', '_FILES', '_COOKIE', '_SERVER', '_ENV', '_SESSION');
                
                // Same effect as disabling register_globals
                foreach($GLOBALS as $key => $value)
                {
                    if( ! in_array($key, $preserve))
                    {
                        global $$key;
                        $$key = NULL;
                        
                        unset($GLOBALS[$key], $$key);
                    }
                }
            }
            
            // Sanitize global data
            
            if(is_array($_POST))
            {
                foreach($_POST as $key => $value)
                {
                    $_POST[$this->clean_input_keys($key)] = $this->clean_input_data($value);
                }
            }
            else
            {
                $_POST = array();
            }
            
            if(is_array($_GET))
            {
                foreach($_GET as $key => $value)
                {
                    $_GET[$this->clean_input_keys($key)] = $this->clean_input_data($value);
                }
            }
            else
            {
                $_GET = array();
            }
            
            if(is_array($_COOKIE))
            {
                foreach($_COOKIE as $key => $value)
                {
                    $_COOKIE[$this->clean_input_keys($key)] = $this->clean_input_data($value);
                }
            }
            else
            {
                $_COOKIE = array();
            }
            
            // Just make REQUEST a merge of POST and GET. Who really wants cookies in it anyway?
            $_REQUEST = array_merge($_GET, $_POST);
            
            self::$instance = $this;
        }
    }
    
    /**
     * Cross site filtering (XSS). Recursive.
     *
     * @param  string Data to be cleaned
     * @return mixed
     */
    public function xss_clean($data)
    {
        // If its empty there is no point cleaning it :\
        if(empty($data))
            return $data;
            
        // Recursive loop for arrays
        if(is_array($data))
        {
            foreach($data as $key => $value)
            {
                $data[$key] = $this->xss_clean($data);
            }
            
            return $data;
        }
        
        // http://svn.bitflux.ch/repos/public/popoon/trunk/classes/externalinput.php
        // +----------------------------------------------------------------------+
        // | Copyright (c) 2001-2006 Bitflux GmbH                                 |
        // +----------------------------------------------------------------------+
        // | Licensed under the Apache License, Version 2.0 (the "License");      |
        // | you may not use this file except in compliance with the License.     |
        // | You may obtain a copy of the License at                              |
        // | http://www.apache.org/licenses/LICENSE-2.0                           |
        // | Unless required by applicable law or agreed to in writing, software  |
        // | distributed under the License is distributed on an "AS IS" BASIS,    |
        // | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or      |
        // | implied. See the License for the specific language governing         |
        // | permissions and limitations under the License.                       |
        // +----------------------------------------------------------------------+
        // | Author: Christian Stocker <chregu@bitflux.ch>                        |
        // +----------------------------------------------------------------------+
        
        // Fix &entity\n;
        $data = str_replace(array('&','<','>'), array('&amp;','&lt;','&gt;'), $data);
        $data = preg_replace('/(&#*\w+)[\x00-\x20]+;/u', '$1;', $data);
        $data = preg_replace('/(&#x*[0-9A-F]+);*/iu', '$1;', $data);
        $data = html_entity_decode($data, ENT_COMPAT, 'UTF-8');

        // Remove any attribute starting with "on" or xmlns
        $data = preg_replace('#(<[^>]+?[\x00-\x20"\'])(?:on|xmlns)[^>]*+>#iu', '$1>', $data);

        // Remove javascript: and vbscript: protocols
        $data = preg_replace('#([a-z]*)[\x00-\x20]*=[\x00-\x20]*([`\'"]*)[\x00-\x20]*j[\x00-\x20]*a[\x00-\x20]*v[\x00-\x20]*a[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2nojavascript...', $data);
        $data = preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*v[\x00-\x20]*b[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2novbscript...', $data);
        $data = preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*-moz-binding[\x00-\x20]*:#u', '$1=$2nomozbinding...', $data);

        // Only works in IE: <span style="width: expression(alert('Ping!'));"></span>
        $data = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?expression[\x00-\x20]*\([^>]*+>#i', '$1>', $data);
        $data = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?behaviour[\x00-\x20]*\([^>]*+>#i', '$1>', $data);
        $data = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:*[^>]*+>#iu', '$1>', $data);

        // Remove namespaced elements (we do not need them)
        $data = preg_replace('#</*\w+:\w[^>]*+>#i', '', $data);

        do
        {
            // Remove really unwanted tags
            $old_data = $data;
            $data = preg_replace('#</*(?:applet|b(?:ase|gsound|link)|embed|frame(?:set)  ?|i(?:frame|layer)|l(?:ayer|ink)|meta|object|s(?:c  ript|tyle)|title|xml)[^>]*+>#i', '', $data);
        }
        while ($old_data !== $data);
        
        return $data;
    }
    
    /**
     * Enforces W3C specifications to prevent malicious exploitation.
     *
     * @param  string Key to clean
     * @return string
     */
    protected function clean_input_keys($data)
    {
        $chars = PCRE_UNICODE_PROPERTIES ? '\pL' : 'a-zA-Z';
        
        if ( ! preg_match('#^[' . $chars . '0-9:_.-]++$#uD', $data))
        {
            exit('Illegal key characters in global data');
        }
        
        return $data;
    }
    
    /**
     * Escapes data.
     *
     * @param  mixed Data to clean
     * @return mixed
     */
    protected function clean_input_data($data)
    {
        if(is_array($data))
        {
            $new_array = array();
            foreach($data as $key => $value)
            {
                $new_array[$this->clean_input_keys($key)] = $this->clean_input_data($value);
            }
            
            return $new_array;
        }
        
        if($this->magic_quotes_gpc === TRUE)
        {
            // Get rid of those pesky magic quotes!
            $data = stripslashes($data);
        }
        
        $data = $this->xss_clean($data);
        
        return $data;
    }
}
?>

Classe vi validazione Server-Side "PHP Validation Class"

<?php
define("vSTRING", 1);		// any string that doesn't have control characters (ASCII 0 - 31) but spaces are allowed
define("vALPHA", 2);		// only letters a-z and A-Z
define("vDIGIT", 3);		// only numbers 0-9
define("vALNUM", 4);		// letters and numbers
define("vINTEGER", 5);		// only numbers 0-9 and an optional - (minus) sign (in the beginning only)
define("vFILENAME", 6);		// a valid file name (including dots but no slashes and other forbidden characters)
define("vBOOL", 7);			// a boolean (TRUE is either a case-insensitive "true" or "1". Everything else is FALSE)
define("vVARIABLE", 8);		// a valid variable name (letters, digits, underscore)
define("vPASSWORD", 9);		// a valid password (alphanumberic + some other characters but no spaces: ASCII 33 - 126)
define("vURL", 10);			// a valid URL (http connection is used to check if url exists!)
define("vEMAIL", 11);		// a valid email address (only checks for valid format: xxx@xxx.xxx)
define("vTEXT", 12);		// *NEW* like vSTRING, but newline characters are allowed

class Validator
{
	var $placeHolder;
	
	function Validator()
	{
		$this -> placeHolder = 0;
	}

	function get_valid($param, $type, $def)
	{
		$value = $param;
		if(!isset($value)) return $def;
		// strip slashes if they've been added by magic_quotes_gpc
		if(get_magic_quotes_gpc())
			$value = stripslashes($value);
		if($value === "") return $def;

		$is_valid = FALSE;
		switch($type)
		{
			case vSTRING:
				if(preg_match("/^[^\x-\x1F]+$/", $value)) $is_valid = TRUE;
				break;
			case vALPHA:
				if(preg_match("/^[a-z]+$/i", $value)) $is_valid = TRUE;
				break;
			case vDIGIT:
				if(preg_match("/^[0-9]+$/", $value))
				{
					$is_valid = TRUE;
					settype($value,"integer");
				}
				break;
			case vALNUM:
				if(preg_match("/^[a-z0-9]+$/i", $value)) $is_valid = TRUE;
				break;
			case vINTEGER:
				if(preg_match("/^-?[0-9]+$/", $value))
				{
					$is_valid = TRUE;
					settype($value, "integer");
				}
				break;
			case vFILENAME:
				if(preg_match("{^[^\\/\*\?\:\,]+$}", $value)) $is_valid = TRUE;
				break;
			case vBOOL:
				if(preg_match("/^true$|^1$/i", $value))
				{
					$is_valid = TRUE;
					$value = TRUE;
				}
				break;
			case vVARIABLE:
				if(preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*$/i", $value)) $is_valid = TRUE;
				if($value == "_") $is_valid = FALSE;
				break;
			case vPASSWORD:
				if(preg_match("/^[\41-\176]+$/", $value)) $is_valid = TRUE;
				break;
			case vURL:
				$href = $value;
				if(!preg_match("/^[a-z]+:/i", $href))
					$href = "http://".$href;
				if(preg_match("/^http:\/\//", $href))
					if($fp = @fopen($href, "r"))
					{
						$is_valid = TRUE;
						$value = $href;
						@fclose($fp);
					}
				break;
			case vEMAIL:
				if(preg_match("/^[a-z0-9_\.-]+@([a-z0-9]+([\-]+[a-z0-9]+)*\.)+[a-z]{2,7}$/i", $value)) $is_valid = TRUE;
				break;
			case vTEXT:
				if(preg_match("/^([^\x-\x1F]|[\r\n])+$/", $value)) $is_valid = TRUE;
				break;
		}
		
		if(!$is_valid) return false;
		else return true;
	}
}
?>

Non mi dilungherò molto nella spiegazione dell'utilizzo delle due classi, prendiamo sempre come esempio lo script errato che c'era ad inizio post e correggiamolo assieme, integrando queste due classi, e facendo un pò più attenzione :)

<?php
    // Connessione al db e inclusioni delle classi (sicurezza e validazione)
    include 'security.class.php';
    include 'validator.class.php';

    // Faccio partire i primi controlli (veloci, senza error handling)
    $security = Security::instance();
    $__GET = $security -> xss_clean($_GET); // -> assegnato nuovo nome a $_GET
    $v = new Validator();
    if ( !($v -> get_valid($__GET['id_passato_da_get'], vDIGIT, "")) ) exit();
    
    $sql = "SELECT * FROM tabella WHERE id='" . $__GET['id_passato_da_get'] . "'";
    $rs = mysql_query($sql);
    if ( $rs && mysql_num_rows($rs) == 1 ) {
        $rw = mysql_fetch_assoc($rs);
        header('Content-Type:text/xml');
        echo '<?xml version="1.0" encoding="ISO-8859-1"?><root><dati>' . $rw['prova'] .'</dati></root>';
    }
    exit();
?>

In questo esempio abbiamo istanziato la classe di sicurezza, abbiamo pulito la variabile superglobale $_GET per prevenire XSS, e abbiamo fatto una validazione in modo che l'unico parametro accettato sia un numero. Tutto il resto infatti può essere considerato un tentativo per bucare lo script :)

Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2009, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Quello che vedete sulla destra di questo sito è un calendario particolare, nel senso che è progettato per il funzionamento asincrono tramite ajax. In questa maniera si può facilmente integrare un modulo calendario in qualsiasi sito con pochissime modifiche e separando nettamente la parte "intelligente" del programma, cioè la creazione dei giorni a partire da un dato mese/anno, dalla parte "meno intelligente", quella di output verso il browser.

Ecco quindi spiegato il funzionamento, separato in:

  • Script PHP Server-Side che crea l'xml con i dati del mese/anno
  • Script client-side in Javascript per iniettare il markup del calendario verso il browser
  • Un esempio di markup HTML per rendere funzionante il tutto

Codice PHP server-side (Creazione dell'xml della coppia mese/anno richiesta)

<?php
	$mese = $_GET['month'];
	$anno = $_GET['year'];
	
	$primoGiornoMese = date( "w", mktime (0, 0, 0, $mese, 1, $anno) );
	
	if( $primoGiornoMese != 1 ) {
		if( $primoGiornoMese == 0 ) $counter = 6;
		else $counter = $primoGiornoMese - 1;
		
		if( $mese == 1 ) { $vecchioMese = 12; $vecchioAnno = $anno - 1; } else { $vecchioMese = $mese -1; $vecchioAnno = $anno; }
		
		$giorniMesePrecedente = date( "t", mktime(0, 0, 0, $vecchioMese, 1, $vecchioAnno) );
		for($i = $counter - 1; $i >= 0; $i--) {
			$curr = mktime(0, 0, 0, $vecchioMese, $giorniMesePrecedente - $i, $vecchioAnno);
			$tArr = Array('mod' => 'oldmonth', 'day' => $giorniMesePrecedente - $i, 'changeRow' => false);
			$calendario[] = $tArr;
		}
	}
	
	$curr = mktime(0, 0, 0, $mese, 1, $anno);
	$continua = true;
	$k = 0;
	while( $continua ) {
		$flagStile = $cambiaRiga = false;
		
		$giornoCorrente = date( "w", $curr );
		$numeroCorrente = date( "d", $curr );
		if( $curr == mktime(0, 0, 0, date("m"), date("d"), date("Y")) ) $flagStile = 'currentDay';
		else {
			if( $giornoCorrente != 6 && $giornoCorrente != 0 ) $flagStile = 'normalDay';
			else $flagStile = 'weekendDay';
		}
		
		if( $giornoCorrente == 0 ) $cambiaRiga = true;
		
		$tArr = Array('mod' => $flagStile, 'day' => $numeroCorrente, 'changeRow' => $cambiaRiga);
		$calendario[] = $tArr;
		
		$curr = mktime(0, 0, 0, $mese, date("d", $curr) + 1, $anno);
		
		if( date("m", $curr) > $mese || ( date("m", $curr) == 1 && $mese == 12 ) || $k >= 50) $continua = false;
		$k++;
	}
	$giorniCodestoMese = date("t", mktime(0, 0, 0, $mese, 1, $anno));
	if( ( $giornoFinaleMese = date("w", mktime(0, 0, 0, $mese, $giorniCodestoMese, $anno)) ) != 0 ) {
		for($i = 1; $i <= (7 - $giornoFinaleMese); $i++) {
			$tArr = Array('mod' => 'oldmonth', 'day' => $i);
			$calendario[] = $tArr;
		}
	}
	
	header('Content-Type:text/xml');
	echo '<?xml version="1.0" encoding="UTF-8" ?>' . chr(10);
	echo '<root>' . chr(10);
	echo '<monthInfo><month>' . substr( ucfirst(itdate("F", mktime(0, 0, 0, $mese, 1, $anno))), 0, 3 ) . '</month><year>' . $anno . '</year></monthInfo>' . chr(10);
	
	foreach( $calendario as $giorno => $valoriGiorno ) {
		$mod = $valoriGiorno['mod'];
		$day = number_format($valoriGiorno['day'], 0, "", "");
		$alt = $valoriGiorno['alt'];
		$title = $valoriGiorno['title'];
		$link = $valoriGiorno['link'];
		$changeRow = ( $valoriGiorno['changeRow'] == false ) ? 0 : 1;
		
		echo '<giorno mod="' . $valoriGiorno['mod'] . '" day="' . number_format($valoriGiorno['day'], 0, "", "") . '" changeRow="' . $changeRow . '"></giorno>' . chr(10);
	}
	echo '</root>' . chr(10);
	
	function itdate ($date_format="l j F Y - H:i:s", $time=0) {//, $date_format="l j F Y - H:i:s", $lang=it 

		$hourdiff = "0";
		$timeadjust = ($hourdiff * 60 * 60);

		if (! $time) $time = time()+$timeadjust;
		if (! $lang) $lang = "It";
		if (! $date_format) 
		{ 
			$date_format = "l j F Y - H:i:s"; 
		}

		$week_days_long = array('Sunday' => 'Domenica', 'Monday' => 'Lunedì ', 'Tuesday' => 'Martedì',
			'Wednesday' => 'Mercoledì', 'Thursday' => 'Giovedì', 'Friday' => 'Venerdì',
			'Saturday' => 'Sabato');
		$months_long = array('January' => 'Gennaio', 'February' => 'Febbraio',
			'March' => 'Marzo', 'April' => 'Aprile','May' => 'Maggio', 'June' => 'Giugno',
			'July' => 'Luglio', 'August' => 'Agosto', 'September' => 'Settembre',
			'October' => 'Ottobre', 'November' => 'Novembre', 'December' => 'Dicembre');
		$week_days_short = array('Sun' => 'Dom', 'Mon' => 'Lun', 'Tue' => 'Mar', 'Wed'=>'Mer',
			'Thu' => 'Gio', 'Fri' => 'Ven', 'Sat' => 'Sab');
		$months_short = array('Jan' => 'Gen', 'Feb' => 'Feb', 'Mar' => 'Mar', 'Apr' => 'Apr',
			'May' => 'Mag', 'Jun' => 'Giu', 'Jul' => 'Lug', 'Aug' => 'Ago',
			'Sep' => 'Set', 'Oct' => 'Ott', 'Nov' => 'Nov', 'Dec' => 'Dic');

		$clock = date($date_format, $time);
		
		if(preg_match("/F/", $date_format) && ($lang != "En")) {
			$model = date("F", $time);
			$replace = $months_long[date("F", $time)];
			$clock = preg_replace("/$model/", $replace, $clock);
		}
		if(preg_match("/l/", $date_format) && ($lang != "En")) {
			$model = date("l", $time);
			$replace = $week_days_long[date("l", $time)];
			$clock = preg_replace("/$model/", $replace, $clock);
		}
		if(preg_match("/M/", $date_format) && ($lang != "En")) {
			$model = date("M", $time);
			$replace = $months_short[date("M", $time)];
			$clock = preg_replace("/$model/", $replace, $clock);
		}
		if(preg_match("/D/", $date_format) && ($lang != "En")) {
			$model = date("D", $time);
			$replace = $week_days_short[date("D", $time)];
			$clock = preg_replace("/$model/", $replace, $clock);
		}
		
		return strtolower($clock);
	}
?>

Codice Javascript per l'iniettamento del markup del calendario verso il browser

/* Calendar Handler JS client-side script v1.1 */

/* 

CONFIGURAZIONE:

Cambiare i valori delle variabili di configurazione, sapendo che:
_id_contenitore_calendario = ID del contenitore che andrà a contenere il calendario. E' un div che conterrà solo una tabella. SPECIFICARE IL # prima dell'id.
_id_contenitore_header_mese_anno = ID del contenitore che conterrà la coppia mese-anno sopra la tabella del mese.
_classe_cella_mesenoncorrente = classe css da applicare alle celle che NON sono del mese corrente, ma che mandiamo in output per completezza
_classe_cella_mesecorrente = classe css da applicare alle celle che sono del mese corrente
_classe_cella_weekend = classe css da applicare alle celle che sono sabato o domenica del mese corrente
_classe_cella_giornocorrente = classe css da applicare nel caso in cui una cella-giorno del calendario corrisponda al giorno corrente

ESEMPIO MARKUP HTML PER RENDERE FUNZIONANTE IL MODULO CALENDARIO: 
<div id="contenitore_tabella_calendario">
	<p id="header_tabella_calendario"></p>
	<table>
		<tbody></tbody>
	</table>
</div>

*/
	// Inizializzazione variabili
	_id_contenitore_calendario = '#contenitore_tabella_calendario';
	_id_contenitore_header_mese_anno = '#header_tabella_calendario';
	_classe_cella_mesenoncorrente = 'oldmonth';
	_classe_cella_mesecorrente = 'normalmonth';
	_classe_cella_weekend = 'weekendday';
	_classe_cella_giornocorrente = 'currentday';
	
	//Gestione Data Odierna
	var today = new Date();
	var mese = today.getMonth() + 1;
	var anno =  today.getFullYear();
	
	// Trigger delle funzioni da far partire all'avvio
	$(document).ready(function() {
		$.ajax({
			type: "GET",
			url: "xml_calendar_creator.php",
			data: "month=" + mese + "&year=" + anno,
			dataType: 'xml', 
			success: parseXmlCalendario, 
			error: function (a, b, e) {
				throw(e);
			}
		});
	});
	
	parseXmlCalendario = function(xml) {
		// Variabile della classe delle celle per la tabella del calendario
		var cellClass = "";
		
		// Iniettamento del titolo del calendario (Mese e Anno)
		$(_id_contenitore_header_mese_anno).html( $(xml).find('monthInfo month').text() + " " + $(xml).find('monthInfo year').text());
		
		// Iniettamento del codice html completo per il calendario
		
		var htmlCalendario = '<tr>';
		$(xml).find('giorno').each(function() {
			var mod = $(this).attr('mod');
			var giorno = $(this).attr('day');
			var cambiaRiga = $(this).attr('changeRow');
			
			switch ( mod ) {
				case 'oldmonth':
					cellClass = _classe_cella_mesenoncorrente;
					break;
				case 'normalDay':
					cellClass = _classe_cella_mesecorrente;
					break;
				case 'currentDay':
					cellClass = _classe_cella_giornocorrente;
					break;
				case 'weekendDay':
					cellClass = _classe_cella_weekend;
					break;
				default: break;
			}
			
			htmlCalendario +=  '<td class="' + cellClass + '" align="center">' + giorno + '</td>';
			if ( cambiaRiga == 1 ) htmlCalendario += '</tr><tr>';
		});
		htmlCalendario += '</tr>';
		
		$(_id_contenitore_calendario + ' tbody').html(htmlCalendario);
	}

Esempio di markup HTML per rendere il tutto funzionante

<!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="en" lang="en">
<head>
	<title>Prova</title>
	<script type="text/javascript" src="jquery.js"></script>
	<script type="text/javascript" src="calendar_js.js"></script>
</head>
<body>
	<div id="contenitore_tabella_calendario">
		<p id="header_tabella_calendario"></p>
		<table>
			<tbody></tbody>
		</table>
	</div>
</body>
</html>
Mischiare il tutto

Il codice PHP può essere salvato con un nome qualsiasi, solo bisogna ricordarsi di andare a modificare il file javascript e, sotto la parte di creazione della richiesta ajax, andare a modificare il nome della variabile che contiene le impostazioni della richiesta.

Ricordo che questo sistema implica forzatamente l'utilizzo della libreria jQuery per poter funzionare. E' chiaramente visibile nel terzo snippet di codice HTML, dove è specificato il richiamo del file jquery.js

Mostrare un messaggio non invasivo con IE6

Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2009, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

La direzione che si sta cercando di seguire, parlando in termini di "standardizzazione" dei formati relativi alle pagine web, è quella di lasciar cadere piano piano l'utilizzo di Internet Explorer 6, che è rinomato per la sua non uniformazione agli standard odierni relativi ai fogli di stile CSS.
Questo fatto implica infatti un notevole impegno di tempo da parte degli sviluppatori web per cercare di "adattare" una pagina web - quindi un layout con un determinato foglio di stile - per il suo funzionamento pseudocorretto anche se viene visualizzata con Internet Explorer 6.

Gli strumenti che gli sviluppatori web hanno a dispozizione per far cessare l'utilizzo di Internet Explorer 6 sono molto pochi, e solo il tempo molto probabilmente consentirà un graduale abbandono di questo datato browser. E' altresì vero comunque che gli stessi sviluppatori web possono dare una mano nella velocizzazione di questo processo. Vediamo come.

Se avete sotto mano Internet Explorer 6, provate a visualizzare l'homepage di questo sito, ovvero mauriziofonte.it. In alto vi verrà mostrato un messaggio non invasivo che vi consiglierà l'aggiornamento del vostro browser. Nel caso specifico, nel messaggio presente in questo sito ho deciso di consigliare Mozilla Firefox, perchè personalmente lo ritengo un ottimo browser. Cliccando sul messaggio, verrete infatti reindirizzati alla pagina di scaricamento di Firefox, ovvero mozilla-europe.org/it/firefox/

Avere un messaggio del genere anche sul vostro sito è molto semplice, e servono pochissimi passaggi per creare questo effetto. Vediamo cosa serve per installarlo, nel dettaglio.

Il codice HTML del messaggio
<!-- DA PIAZZARE ALL'INIZIO DELLA PAGINA, SOTTO IL TAG <body> -->
<div id="ie6warningHolder">
    <a href="javascript:void(0)" id="ie6warning">
        <span class="text">Sembra che tu stia ancora utilizzando IE6. Passa adesso a Firefox per visualizzare correttamente le tue pagine web preferite!</span>
        <span class="closer" alt="Chiudi Avviso" title="Chiudi Avviso">X</span>
    </a>
</div>
Il foglio di stile per il div del messaggio
/* DA AGGIUNGERE AL VOSTRO FOGLIO DI STILE */
#ie6warning { padding: 0; margin: 0; font-size: 11px; display: block; width:100%; height: 21px; text-decoration: none; background: #FFFFE1 url(IE6alert.gif) no-repeat 5px center; border-bottom: 1px solid #898679; }
#ie6warning span.text { margin: 3px 0 0 13px; padding:0; display: block; float: left; width: 80%; cursor: pointer; color: #000; font-weight: normal;  }
#ie6warning span.closer { font-size: 12px; font-weight: bold; margin: 4px 5px 0 0; padding: 0; float:right; text-align: right; display: block; width: 5%; cursor:pointer; }
#ie6warning:hover { background-color: #316AC5; }
#ie6warning:hover span.text { color: #FFF; }
#ie6warning:hover span.closer { color: #CCC; }

Il file immagine da me utilizzato che si chiama IE6alert.gif è disponibile per il download qui: IE6alert.gif

Il codice Javascript/Jquery che mostra e nasconde il div
// DA AGGIUNGERE ALLA VOSTRA LIBRERIA JSCRIPT
$('#ie6warning span.text').click(function() {
	window.open('http://www.mozilla-europe.org/it/firefox/','Firefox');
	$('#ie6warningHolder').slideUp(2000);
});
$('#ie6warning span.closer').click(function() {
	$('#ie6warningHolder').slideUp(2000);
});

Per installarlo sulla vostra pagina, è sufficiente copiare il codice HTML del messaggio nel vostro template, è sufficiente aggiungere il codice CSS del div contenitore #ie6warning all'interno del CSS del vostro sito, e stessa cosa per il codice javascript. Dovrete avere però già abilitata e disponibile la libreria jQuery.

Così facendo, però, il messaggio sarà sempre visibile! Come fare quindi per mostrarlo SOLO nel caso in cui l'utente stia visualizzando la pagina con IE6??
Molto semplice. Basta fare un controllo lato server.
Per il mio caso specifico, mi sono basato su una classe di browser detection molto semplice ed intuitiva.
Una volta installata sul server, basterà inserire un codice php simile a questo all'interno delle routine di calcolo delle pagine:
$br = new Browser;
if ( $br -> Name == 'MSIE' && ($br -> Version <= 6) ) {
	$messaggio_IE6 = '<div id="ie6warningHolder"><a href="javascript:void(0)" id="ie6warning"><span class="text">Sembra che tu stia ancora utilizzando IE6. Passa adesso a Firefox per visualizzare correttamente le tue pagine web preferite!</span><span class="closer" alt="Chiudi Avviso" title="Chiudi Avviso">X</span></a></div>';
	echo $messaggio_IE6;
}
Niente di particolarmente difficile. Pochi minuti per avere nel nostro sito un fantastico messaggio assolutamente non invasivo che consiglia l'aggiornamento del browser.
Per qualsiasi dubbio o chiarimento, non esitate a lasciare un commento.

Ridimensionare immagini con PHP: Classe SimpleImage

Postato in Programmazione PHP
Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2009, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Spesso le operazioni più basilari son quelle che richiedono un impegno notevole, in termini di mero tempo di programmazione.
Quante volte vi è infatti capitato di avere a che fare con operazioni con le immagini?
E' un problema in cui si incappa molto frequentemente se si ha a che fare con siti di un certo livello.
Girovagando per la rete, ho trovato una classe molto snella e facile da capire che permette basilari operazioni di ridimensionamento su ogni tipo di file immagine valido.
La classe è stata sviluppata dal sito white-hat-web-design.co.uk, e vi riporto in toto il codice con alcuni commenti in italiano per comprenderla meglio

Codice della classe SimpleImage.php

<?php
/*
* File: SimpleImage.php
* Author: Simon Jarvis
* Copyright: 2006 Simon Jarvis
* Date: 08/11/06
* Link: http://www.white-hat-web-design.co.uk/articles/php-image-resizing.php
* 
* This program is free software; you can redistribute it and/or 
* modify it under the terms of the GNU General Public License 
* as published by the Free Software Foundation; either version 2 
* of the License, or (at your option) any later version.
* 
* This program is distributed in the hope that it will be useful, 
* but WITHOUT ANY WARRANTY; without even the implied warranty of 
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 
* GNU General Public License for more details: 
* http://www.gnu.org/licenses/gpl.html
*
*/
 
class SimpleImage {
   
   var $image;
   var $image_type;
 
   function load($filename) {
      $image_info = getimagesize($filename);
      $this->image_type = $image_info[2];
      if( $this->image_type == IMAGETYPE_JPEG ) {
         $this->image = imagecreatefromjpeg($filename);
      } elseif( $this->image_type == IMAGETYPE_GIF ) {
         $this->image = imagecreatefromgif($filename);
      } elseif( $this->image_type == IMAGETYPE_PNG ) {
         $this->image = imagecreatefrompng($filename);
      }
   }
   function save($filename, $image_type=IMAGETYPE_JPEG, $compression=75, $permissions=null) {
      if( $image_type == IMAGETYPE_JPEG ) {
         imagejpeg($this->image,$filename,$compression);
      } elseif( $image_type == IMAGETYPE_GIF ) {
         imagegif($this->image,$filename);         
      } elseif( $image_type == IMAGETYPE_PNG ) {
         imagepng($this->image,$filename);
      }   
      if( $permissions != null) {
         chmod($filename,$permissions);
      }
   }
   function output($image_type=IMAGETYPE_JPEG) {
      if( $image_type == IMAGETYPE_JPEG ) {
         imagejpeg($this->image);
      } elseif( $image_type == IMAGETYPE_GIF ) {
         imagegif($this->image);         
      } elseif( $image_type == IMAGETYPE_PNG ) {
         imagepng($this->image);
      }   
   }
   function getWidth() {
      return imagesx($this->image);
   }
   function getHeight() {
      return imagesy($this->image);
   }
   function resizeToHeight($height) {
      $ratio = $height / $this->getHeight();
      $width = $this->getWidth() * $ratio;
      $this->resize($width,$height);
   }
   function resizeToWidth($width) {
      $ratio = $width / $this->getWidth();
      $height = $this->getheight() * $ratio;
      $this->resize($width,$height);
   }
   function scale($scale) {
      $width = $this->getWidth() * $scale/100;
      $height = $this->getheight() * $scale/100; 
      $this->resize($width,$height);
   }
   function resize($width,$height) {
      $new_image = imagecreatetruecolor($width, $height);
      imagecopyresampled($new_image, $this->image, 0, 0, 0, 0, $width, $height, $this->getWidth(), $this->getHeight());
      $this->image = $new_image;   
   }      
}
?>
E adesso vediamo un codice di esempio di utilizzo pratico di questa classe, per effettuare le operazioni di ridimensionamento:
<?php
   // Inclusione della classe, dopodichè richiamo il costruttore della classe
   include('SimpleImage.php');
   $image = new SimpleImage();

   // Carica il file picture.jpg, lo ridimensiona a 250x400, e lo salva in picture2.jpg
   $image->load('picture.jpg');
   $image->resize(250,400);
   $image->save('picture2.jpg');

   // Carica il file picture.jpg e riscala l'immagine di output con larghezza massima a 250px, salvandola in picture2.jpg
   $image->load('picture.jpg');
   $image->resizeToWidth(250);
   $image->save('picture2.jpg');

   // Carica il file picture.jpg e riscala l'immagine di output al 50%, salvandola in picture2.jpg
   $image->load('picture.jpg');
   $image->scale(50);
   $image->save('picture2.jpg');

   // Carica il file picture.jpg, lo riscala con larghezza massima a 150px, e lo manda in output al browser (notare l'Header)
   header('Content-Type: image/jpeg');
   $image->load('picture.jpg');
   $image->resizeToWidth(150);
   $image->output();
?>
Molto semplice ed immediato. In futuro proverò ad estendere la classe per abilitare operazioni come blur, crop, settaggio contrasto e luminosità, e magari anche watermarking.
Attenzione! Questo contenuto è vecchioQuesto articolo risale al 2009, quindi i contenuti e le operazioni qui consigliate potrebbero essere diventate obsolete nel corso del tempo.

Chi amministra un server web sa quanto è importante il discorso del salvataggio di quanto è stato creato, sia a livello di database sia a livello di file, di righe di codice scritte.

Per fortuna alcuni svegli programmatori di ngcoders.com hanno pensato a questo problema e hanno creato un piccolissimo script in grado di creare una copia di backup del filesystem e del database in una sola passata.
Sinceramente non ho trovato il nome di questo script, penso che lo chiamerò con un nome eloquente come 1-click filesystem and database backup tool

Il file .zip scaricato si compone di 3 file principali, scritti in php:

  • backup.php : lo script principale da richiamare per avviare il processo di backup
  • functions.php : le classi e le funzioni principali che creano fisicamente i backup
  • config.php : il file di configurazione semplice nel quale specificare le opzioni del backup che vogliamo ottenere

Il file di configurazione è assai semplice, come ho sottolineato prima, ecco una anteprima e qualche istruzione per completarlo:
<?php
// Quali directory/file da backuppare ( i nomi delle directory con lo slash iniziale )
// e' un array, quindi ricordatevi la virgola dopo ogni elemento tra le virgolette
$configBackup = array('../');
// Quali directory da saltare durante il processo di backup
$configSkip   = array('backup/');  
// Dove piazzare i file di backup a completamento dei processi? (ricordarsi lo slash finale)
$configBackupDir = 'backup/';
// I database dei quali effettuare il backup. Possono essere multipli. (Se l'array delle tabelle contiene delle entry, verranno salvate SOLO quelle tabelle!)
$configBackupDB[] = array(
'server' => 'localhost',
'username' => 'root',
'password' => '',
'database' => 'databasename',
'tables' => array('tabella1', 'tabella2', 'tabella3')
);
// Se vogliamo che il backup ci venga spedito via posta elettronica, specifichiamo un indirizzo valido
$configEmail = 'test@test.example.com';
?>

Niente di più semplice e niente di più funzionale: uno di quei tool che un Web Developer dovrebbe sempre tenere sotto mano, una sorta di chiave inglese da 10 del programmatore php/mysql

Ecco il link per scaricare il programma: script per la creazione di backup di file e database
Buon lavoro!