Installation d’aegir pour gérer un parc de sites Drupal

aegir est un outil web qui permet de piloter un parc de sites Drupal. Ce qu’on fait en général via des commandes drush (drush updb, drush fra, etc…) on peut le faire via un site web directement. C’est un site Drupal, qui pilote la gestion d’autres sites Drupal. Bien sûr, on pourrait faire tout ça et même plus via des scripts ou quelques outils de supervision.

L’intérêt c’est de donner à l’utilisateur (le client) la possibilité de créer des sites à la volé par exemple, ou de lancer des opérations sur plusieurs sites à la fois, sans « mettre les mains dans le cambouis ».

Il s’agit d’une installation mono-serveur dans le cas présent, sur une machine virtuelle contenant déjà un ensemble de sites Drupal qu’on souhaite migrer vers aegir afin de pouvoir gérer les sites dessus (mise à jour, clonage, backup).

Installation d’aegir

Je me place dans un contexte debian/apache/mysql/php classique pour un drupal 7 (ou 8). Je supposes que vous avez déjà installé drush.

Avant toute chose, il faut s’assurer de 3 choses :

  • vous disposez du mot de passe root de MySQL
  • vous êtes sudoer
  • vous avez un nom de domaine (optionnel mais plus propre) aegir.mondomaine.com ou aegir-mondomaine.com de disponible.

Pour installer aegir, c’est simple :

echo "deb http://debian.aegirproject.org stable main" | sudo tee -a /etc/apt/sources.list.d/aegir-stable.list
curl http://debian.aegirproject.org/key.asc | sudo apt-key add -
sudo apt-get update
sudo apt-get install aegir3

Optionnellement vous pouvez faire un ‘sudo mysql_secure_installation’ au préalable pour plus de sécurité (pour ma part, je fais confiance au sysadmin qui a installé la VM).

A partir de la, l’installateur va vous demander un nom de domaine pour le futur site aegir et le mot de passe root mysql pour créer sa base donnée.

Vérification de l’installation

Une fois l’installation effectuée, vous obtenez ceci :

  • un user linux aegir
  • son répertoire home avec tout une install dedans : /var/aegir
  • un nouveau vhost
# apachectl -S
VirtualHost configuration:
wildcard NameVirtualHosts and _default_ servers:
*:80                   is a NameVirtualHost
         default server default (/etc/apache2/conf.d/aegir.conf:5)
         port 80 namevhost default (/etc/apache2/conf.d/aegir.conf:5)
         port 80 namevhost aegir.monsite.fr (/var/aegir/config/server_master/apache/vhost.d/aegir.monsite.fr:1)

Aegir va automatiquement créer des alias drush pour vos nouveaux sites. Mais, pour les obtenir, il faut être loggué en tant qu’aegir. Donc au lieu d’un drush status il faut faire :

su - aegir -c 'drush status'
 PHP configuration      :  /etc/php5/cli/php.ini                                                                                                                                                       
 PHP OS                 :  Linux
 Drush script           :  /usr/local/bin/drush
 Drush version          :  8.1.2
 Drush temp directory   :  /tmp
 Drush configuration    :  /var/aegir/.drush/drushrc.php
 Drush alias files      :  /var/aegir/.drush/monsite.fr.alias.drushrc.php /var/aegir/.drush/hm.alias.drushrc.php /var/aegir/.drush/server_master.alias.drushrc.php  
                           /var/aegir/.drush/hostmaster.alias.drushrc.php /var/aegir/.drush/platform_hostmaster.alias.drushrc.php /var/aegir/.drush/server_localhost.alias.drushrc.php                

So far so good.

Nous verrons dans un prochain article comment rajouter des sites existant au sein d’aegir.

Retex : sécurisation Drupal

Je me suis fait hacker sur l’un de mes sites Drupal en production, sur le serveur qui était mal protégé.

L’alerte

Tout commence par OVH qui bloque l’envoies d’email parce que le serveur est repéré comme spammeur :

Bonjour,

Notre protection Anti-Spam a détecté un envoi important de spam à partir d'une de vos IP: 
92.XX.XX.XXX

Afin d'assurer la sécurité de notre réseau le trafic sortant de votre serveur vers les
ports 25 a été suspendu.

Afin que vous puissiez effectuer les vérifications voici un échantillon des emails bloqués:

Destination IP: 217.69.139.150 - Message-ID: <span id="OBJ_PREFIX_DWT169_com_zimbra_email" class="Object"><span id="OBJ_PREFIX_DWT170_com_zimbra_email" class="Object">20160325090323.E07981XXXXX@vpsXXXXX.ovh.net</span></span> - Spam score: 9999
Destination IP: 66.102.1.27 - Message-ID: <span id="OBJ_PREFIX_DWT171_com_zimbra_email" class="Object"><span id="OBJ_PREFIX_DWT172_com_zimbra_email" class="Object">20160325090402.E91EE1XXXXX@vpsXXXXX.ovh.net</span></span> - Spam score: 9999
Destination IP: 194.186.47.93 - Message-ID: <span id="OBJ_PREFIX_DWT173_com_zimbra_email" class="Object"><span id="OBJ_PREFIX_DWT174_com_zimbra_email" class="Object">20160325090343.81C801XXXXX@vpsXXXXX.ovh.net</span></span> - Spam score: 9999
Destination IP: 94.100.180.150 - Message-ID: <span id="OBJ_PREFIX_DWT175_com_zimbra_email" class="Object"><span id="OBJ_PREFIX_DWT176_com_zimbra_email" class="Object">20160325090422.973121XXXXX@vpsXXXXX.ovh.net</span></span> - Spam score: 9999
Destination IP: 94.100.180.150 - Message-ID: <span id="OBJ_PREFIX_DWT177_com_zimbra_email" class="Object"><span id="OBJ_PREFIX_DWT178_com_zimbra_email" class="Object">20160325090441.E3F061XXXXX@vpsXXXXX.ovh.net</span></span> - Spam score: 9999

Merci de consulter attentivement ce guide:

    <span id="OBJ_PREFIX_DWT179_com_zimbra_url" class="Object"><span id="OBJ_PREFIX_DWT180_com_zimbra_url" class="Object"><a href="http://guide.ovh.net/AntiSpamBestPratice" target="_blank">http://guide.ovh.net/AntiSpamBestPratice</a></span></span>

Si vous avez identifié et résolu la cause du blocage, vous pouvez débloquer votre IP
depuis le manager, en vous rendant à cette adresse :

    <span id="OBJ_PREFIX_DWT181_com_zimbra_url" class="Object"><span id="OBJ_PREFIX_DWT182_com_zimbra_url" class="Object"><a href="https://www.ovh.com/manager/#/configuration/ip?action=antispam&amp;ip=92.XX.XX.XX&amp;ipSpamming=92.XX.XX.XX" target="_blank">https://www.ovh.com/manager/#/configuration/ip?action=antispam&amp;ip=92.XX.XX.XX&amp;ipSpamming=92.XX.XX.XX</a></span></span>

C’est une IP Russe qui envoies des emails (probablement du SPAM) vers les Chinois. Ce qui signifie que nous avons été hacké.

A partir de la, j’ai fait appel à un sysadmin pour découvrir la faille et protéger le site correctement.

L’audit

L’expert sécurité repère vite l’apparition d’un nouveau thème « fusion core » qui n’a rien à faire la. D’ailleurs git lui même nous le dit :

/var/www/prod/sites/all/themes/fusion/fusion_core/system_ml.php

Après que son robot ai repéré le site, et se soit loggué (l’inscription peut se faire de manière automatique), il passe par le authorise.php pour demander l’installation d’un « theme » qui contient des scripts tels que « shell.php » qui permet ensuite de prendre le contrôle de la machine.
Voici un extrait des logs d’apache :

/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT407_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT408_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:48:12 +0100] "GET / HTTP/1.1" 200 34864 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari
/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT409_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT410_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:48:12 +0100] "POST / HTTP/1.0" 302 680 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/
537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT411_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT412_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:48:13 +0100] "POST /?destination=node/323 HTTP/1.0" 200 193793 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chr
ome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT413_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT414_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:48:20 +0100] "GET /admin/modules HTTP/1.1" 200 68620 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2
454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT415_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT416_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:48:30 +0100] "POST /admin/modules HTTP/1.0" 200 740266 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0
.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT417_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT418_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:49:21 +0100] "GET /admin/appearance/install HTTP/1.1" 200 147306 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) C
hrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT419_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT420_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:49:22 +0100] "POST /admin/appearance/install HTTP/1.0" 302 465 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chr
ome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT421_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT422_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:49:23 +0100] "POST /authorize.php?batch=1&amp;amp;op=start&amp;amp;id=1901 HTTP/1.0" 200 245 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, l
ike Gecko) Chrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT423_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT424_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:49:24 +0100] "POST /authorize.php?batch=1&amp;amp;id=1901&amp;amp;op=do HTTP/1.0" 200 312 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like
  Gecko) Chrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT425_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT426_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:49:25 +0100] "GET /authorize.php?batch=1&amp;amp;id=1901&amp;amp;op=finished HTTP/1.1" 302 297 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML,
  like Gecko) Chrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT427_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT428_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:49:26 +0100] "GET /authorize.php HTTP/1.1" 200 3699 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.24
54.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT429_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT430_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:49:27 +0100] "GET /admin/modules HTTP/1.1" 200 68620 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2
454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT431_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT432_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:49:38 +0100] "POST /admin/modules HTTP/1.0" 200 739926 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0
.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT433_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT434_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:19:50:18 +0100] "GET /sites/all/themes/fusion/fusion_core/shell.php HTTP/1.1" 200 368 "-" "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KH
TML, like Gecko) Chrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.45"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT435_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT437_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:20:05:06 +0100] "POST /sites/all/themes/fusion/fusion_core/shell.php HTTP/1.0" 200 23488 "&lt;span id="OBJ_PREFIX_DWT436_com_zimbra_url" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT438_com_zimbra_url" class="Object"&gt;&lt;a href="http://www.emindhub.com/sites/all/themes/fusion/fusi" target="_blank"&gt;http://www.emindhub.com/sites/all/themes/fusion/fusi&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;
on_core/shell.php" "Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.9.168 Version/11.50"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT439_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT441_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:20:05:06 +0100] "POST /sites/all/themes/fusion/fusion_core/shell.php HTTP/1.0" 200 24083 "&lt;span id="OBJ_PREFIX_DWT440_com_zimbra_url" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT442_com_zimbra_url" class="Object"&gt;&lt;a href="http://www.emindhub.com/sites/all/themes/fusion/fusi" target="_blank"&gt;http://www.emindhub.com/sites/all/themes/fusion/fusi&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;
on_core/shell.php" "Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.9.168 Version/11.50"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT443_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT444_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:20:05:06 +0100] "GET /sites/all/themes/fusion/fusion_core/shell.php HTTP/1.0" 200 24073 "-" "Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.9.168
  Version/11.50"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT445_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT446_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:20:15:41 +0100] "POST /sites/all/themes/fusion/fusion_core/system_ml.php HTTP/1.1" 200 1126 "-" "Mozilla/5.0 (Windows NT 6.1; rv:38.0) Gecko/2
0100101 Firefox/38.0"
/var/log/apache2/access-prod.log.1:52.29.249.200 - - [14/&lt;span id="OBJ_PREFIX_DWT447_com_zimbra_date" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT449_com_zimbra_date" class="Object"&gt;Mar&lt;/span&gt;&lt;/span&gt;/2016:20:15:41 +0100] "POST /sites/all/themes/fusion/fusion_core/system_ml.php HTTP/1.1" 200 1080 "&lt;span id="OBJ_PREFIX_DWT448_com_zimbra_url" class="Object"&gt;&lt;span id="OBJ_PREFIX_DWT450_com_zimbra_url" class="Object"&gt;&lt;a href="http://www.emindhub.com/sites/all/themes/fusion/f" target="_blank"&gt;http://www.emindhub.com/sites/all/themes/fusion/f&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;
usion_core/system_ml.php" "Mozilla/5.0 (Windows NT 6.1; rv:38.0) Gecko/20100101 Firefox/38.0"

Un nslookup nous indique que l’attaque est lancée depuis un serveur amazon (difficile donc d’aller plus loin) :

nslookup 52.29.249.200
Server:        212.27.40.240
Address:    212.27.40.240#53

Non-authoritative answer:
200.249.29.52.in-addr.arpa    name = ec2-52-29-249-200.eu-central-1.compute.amazonaws.com.

Authoritative answers can be found from:
29.52.in-addr.arpa    nameserver = pdns1.ultradns.net.
29.52.in-addr.arpa    nameserver = x1.amazonaws.com.
29.52.in-addr.arpa    nameserver = x3.amazonaws.org.
29.52.in-addr.arpa    nameserver = x4.amazonaws.org.
29.52.in-addr.arpa    nameserver = x2.amazonaws.com.

Veuillez noter le passage par :

/admin/appearance/install
/authorize.php?batch=1&amp;op=start&amp;id=1901
/sites/all/themes/fusion/fusion_core/shell.php

Du coup l’expert a testé le XSS par acquis de conscience :

First Name : guest<script>alert('attacked')</script>
Last Name : <h1>guest<h1>

En effet, il manquait des « check_plain » sur l’affichage des profils utilisateurs.

Mais ce fût loin d’être terminé :

  • répertoires de tests behat et de deploy dans le root drupal qui pouvaient donner accès à des informations importantes sur le site
  • un adminer.php à la racine du VPS, bien pratique pour développer, mais aurais du être à minima derrière un htpasswd
  • sites de dev non protégés derrière un htpasswd (et pour cause, ça empêche les tests behat de fonctionner)
  • une seule VM pour la prod et les devs : du temporaire qui dure.

Actions menées

  • Migration vers un serveur de prod séparé du serveur de dev
  • déplacement de la racine drupal dans ${GITROOT}/www
  • déplacement du répertoire bdd, deploy et autres répertoires non drupal à la racine
  • sécurisation de la conf apache (intégration du htaccess directement dedans + protection accrue des répertoires)
  • accès interdit aux scripts drupal dangereux : install.php, authorize.php
  • répertoire tmp public private
  • mise en place du ssl
  • passage du script « fix perm » sur la racine drupal afin de s’assurer qu’il n’y a pas de répertoires qui sont trop ouverts (ou inaccessibles)

Améliorer Behat pour Drupal avec 3 extensions : screenshot, code coverage, et watchdog

Behat est très puissant pour faire des tests de non regression. Comment le rendre encore plus puissant, avec 3 petites extensions très pratiques pour le debug :

  1. En affichant les warning rajoutés dans le watchdog automatiquement à la fin d’un test. Très pratique pour s’assurer qu’il n’y a pas d’erreur cachées pendant l’exécution des tests
  2. En rajoutant un test de couverture du code avec xdebug et phpcov pour voir si tout est bien testé
  3. En prenant un screenshot automatique de l’étape behat si elle plante, afin de pouvoir voir ou est le problème sans avoir passer par un « Then I break »

Rajouter un display du watchdog à la fin d’un scénario

dans le composer.json de votre répertoire behat (il faut avoir installé composer avant bien sûr)

{
  "require": {
    "drupal/drupal-extension": "~3.0",
    "jorgegc/behat-drupal-extension": "*"
}

dans le shell on fait la mise à jour :

composer update

On rajoute le contexte dans behat.yml

default:
  suites:
    default:
      contexts:
        - JGC\Behat\DrupalExtension\Context\WatchdogContext

Et voila, c’est tout. Il aller chercher un vendor/jorgegc/behat-drupal-extension/src/JGC/Behat/DrupalExtension/Context/WatchdogContext.php qui fait tout le job.

Il suffit de rajouter @watchdog à vos scénarios.

Rajouter un test de couverture de code

dans le composer.json

{
"require": {
"drupal/drupal-extension": "~3.0",
"phpunit/php-code-coverage": "^2.2",
"phpunit/phpcov": "*",
}

Ne pas oublier le « composer update » bien sûr. Et le contexte qui va bien avec features/bootstrap/CoverageContext.php :

use Behat\Behat\Hook\Scope\BeforeScenarioScope;
//require_once('bdd/vendor/phpunit/php-code-coverage/src/CodeCoverage/Filter.php');

/**
 * Created by PhpStorm.
 * User: elie
 * Date: 01/09/15
 * Time: 11:29
 */
class CoverageContext implements Context
{
    /**
     * @var PHP_CodeCoverage
     */
    private static $coverage;

    /** @BeforeSuite */
    public static function setup()
    {
        $filter = new PHP_CodeCoverage_Filter();
        $filter->addDirectoryToBlacklist(__DIR__ . "/../../vendor");
        $filter->addDirectoryToWhitelist(__DIR__ . "/../../src");
        self::$coverage = new PHP_CodeCoverage(null, $filter);
    }

    /** @AfterSuite */
    public static function tearDown()
    {
        $writer = new PHP_CodeCoverage_Report_HTML();
        $writer->process(self::$coverage, __DIR__ . "/../../tmp/coverage");
    }

    private function getCoverageKeyFromScope(BeforeScenarioScope $scope)
    {
        $name = $scope->getFeature()->getTitle() . '::' . $scope->getScenario()->getTitle();
        return $name;
    }

    /**
     * @BeforeScenario
     */
    public function startCoverage(BeforeScenarioScope $scope)
    {
        self::$coverage->start($this->getCoverageKeyFromScope($scope));
    }

    /** @AfterScenario */
    public function stopCoverage()
    {
        self::$coverage->stop();
    }
}

Il faut avoir installé xdebug au préalable. Évidement, en général vos tests behat ne concernent que votre code métier.  phpcov va sûrement vous dire que vous n’avez pas tout testé, et pour cause, vous ne couvrez pas l’ensemble du BO Drupal. Il faudrait donc encore rajouter tous les bons répertoires dans la partie  « addDirectoryToBlacklist ».

Installer xdebug:

sudo apt-get install php5-dev php-pear
sudo pecl install xdebug
find / -name 'xdebug.so' 2> /dev/null

/etc/php5/conf.d/20-xdebug.ini :

zend_extension=/usr/lib/php5/20100525/xdebug.so
xdebug.max_nesting_level=200

La procédure normale n’ayant pas fonctionné dans mon cas (Debian 7).

Screenshot automatique en cas d’erreur

Cette fois ci, c’est un contexte qu’on va rajouter.

Ce n’est pas tout à fait un screenshot qui est fait. Ce n’est pas un .png qui généré, mais une sauvegarde du HTML généré, qu’on peut visualiser dans un navigateur. Et en fait, c’est encore mieux puisque ça nous permet, en plus de voir la page affichée, de voir les éléments HTML.

C’est cette ligne qui fait tout le boulot, le reste ne concerne que l’écriture dans un fichier :

      $html_data = $this->getSession()->getDriver()->getContent();

features/bootstrap/ScreenshotContext.php

<?php

use Drupal\DrupalExtension\Context\RawDrupalContext,
    Drupal\DrupalExtension\Context\DrupalContext;
use Behat\MinkExtension\Context\MinkContext;
use Behat\Behat\Hook\Scope\BeforeScenarioScope,
    Behat\Behat\Hook\Scope\AfterStepScope;

/**
 * Defines application features from the specific context.
 */
class ScreenshotContext extends DrupalContext {

  private $screenshotPath;

  /**
   * Initializes context.
   *
   * Every scenario gets its own context instance.
   * You can also pass arbitrary arguments to the
   * context constructor through behat.yml.
   */
  public function __construct( $tempPath = '/../bdd/tmp', $screenshotPath = '/screenshots', $htmlpagePath = '/behat_page.html' ) {
    $this->tempPath = $tempPath;
    $this->screenshotPath = $screenshotPath;
    $this->htmlPagePath = $htmlpagePath;
  }


 /**
   * Take screen-shot when step fails.
   *
   * @AfterStep
   * @param AfterStepScope $scope
   */
  public function takeScreenshotAfterFailedStep(AfterStepScope $scope)
  {
    // come from : https://github.com/Behat/Behat/issues/649
    // and from : https://gist.github.com/fbrnc/4550079

    global $base_url;

    if (99 === $scope->getTestResult()->getResultCode()) {

      if (! is_dir( $base_url . $this->tempPath . $this->screenshotPath )) {
        mkdir( $base_url . $this->tempPath . $this->screenshotPath, 0777, true );
      }
      $step = $scope->getStep();
            $id = /*$step->getParent()->getTitle() . '.' .*/ $step->getType() . ' ' . $step->getText();
            $id = $scope->getFeature()->getTitle().' '.$step->getLine().'-'.  $step->getType() . ' ' . $step->getText();
            $filename = 'Fail.'.preg_replace('/[^a-zA-Z0-9-_\.]/','_', $id) . '.html';

      $html_data = $this->getSession()->getDriver()->getContent();
      file_put_contents( DRUPAL_ROOT. $this->tempPath . $this->screenshotPath . '/' . $filename, $html_data);
      echo 'Screenshot error at : ' . $base_url . $this->tempPath . $this->screenshotPath . '/' . $filename;
    }
  }
}

Et bien sûr, dans le fichier de conf behat.yml

default:
  suites:
    default:
      contexts:
        - ScreenshotContext

memo YAML

Respecte l’indentation (uniquement via les espaces)

  • avec [ pour les listes
  • : avec { pour les mappings (enregistrements / tableau associatifs)

Pour le multi-ligne c’est un peu compliqué (_ veut dire espace):

  • | et > démarre à la ligne suivante, garde les espaces à la fin
    • | considère les sauts de ligne comme des sauts de ligne et pas des espaces
  • _ et et «  sur la même ligne, élimine les espaces à la fin
    • «  échappe les \n et \ et « 
    • _ si la chaîne ne contient pas # ou :
Scalars
# scalar = value
a: 1
a: 1.234
b: 'abc'
b: "abc"
b: abc
c: false	# boolean type
d: 2015-04-05	# date type

# Enforcing strings
b: !str 2015-04-05
Sequences
# sequence
array:
- 132
- 2.434
- 'abc'

# sqeuence of sequences
my_array:
- [1, 2, 3]
- [4, 5, 6]
Hashes
# Nest hash
my_hash:
  subkey:
    subsubkey1: 5
    subsubkey2: 6
  another:
    somethingelse: 'Important!'

# Hash of hashes
my_hash: {nr1: 5, nr2: 6}
Newlines
# block notation (newlines become spaces)
content:
  Arbitrary free text
  over multiple lines stopping
  after indentation changes...

# literal style (newlines are preserved)
content: |
  Arbitrary free text
  over "multiple lines" stopping
  after indentation changes...

# + indicator (keep extra newlines after block)
content: |+
  Arbitrary free text with two newlines after


# - indicator (remove extra newlines after block)
content: |-
  Arbitrary free text without newlines after it


# folded style (plié newlines are preserved)
content: >
  Arbitrary free text
  over "multiple lines" stopping
  after indentation changes...

Multiple Documents
---
content: doc1
---
content: doc2
Reference Content
---
values:
- &ref Something to reuse
- *ref	# Reused content
Merging Keys
default_settings:
  install:
    dir: /usr/local
    owner: root
  config:
    enabled: false

# Derive settings for 'my_app' from default and change install::owner
my_app_settings:
  <<: *default_settings
  install:
    owner: my_user
Complex Mapping
---
? - key
:
  - value
# Note: key and value can be multiple, complex structures
Tags
%TAG !personne! tag:foo.org,2004:bar
---
- !personne
    nom:    Simpson
    prenom: Omer
explicit_string: !!str 0.5
python_complex_number: !!python/complex 1+2j

Mémo pour les sautes de ligne

> | _ «  >-
Trailing spaces Kept Kept Kept
Single newline => _ \n _ _ _ _
Double newline => \n \n\n \n \n \n \n
In-line newlines No No No \n No No
Appended* \n \n \n
Single quote  »
Double quote «  «  «  \ » «  « 
Backslash \ \ \ \\ \ \
 » # », « : «  Ok Ok No Ok Ok Ok
Can start on same line as key  No No Yes Yes Yes No
  • http://symfony.com/legacy/doc/reference/1_3/fr/02-yaml
  • http://www.yaml.org/refcard.html
  • http://lzone.de/cheat-sheet/YAML
  • https://fr.wikipedia.org/wiki/YAML
  • http://sweetohm.net/article/introduction-yaml.html
  • http://stackoverflow.com/questions/3790454/in-yaml-how-do-i-break-a-string-over-multiple-lines
  • http://learnxinyminutes.com/docs/yaml/

Les plugins de Drupal 8, kaçaçéksa ?

En Drupal 7, tout démarre avec le hooks. Ce temps de simplicité se termine avec Drupal 8, on a maintenant 6 mécanismes d’extension (hooks, plugins, entities, services, routing, events). Les plugins sont les petits nouveaux de l’API drupal 8, ils remplacent certains hooks, mais pas tous. On y est très vite confronté puisque pour créer un block, on passe par un plugin (Drupal\Core\Block\BlockBase). Mais pas pour créer un menu menu (hook_menu qui n’existe plus en Drupal 8) ni modifier une formulaire (hook_form_alter, qui existe toujours en D8). Pourquoi ?

Pourquoi certains hooks sont remplacés et pas d’autres ? Pourquoi les avoir remplacés ? hook, plugin, services, avec tous ces mécanismes d’extensions, on s’y perds. Qui sert à quoi ? Je me suis très tôt posé des questions la dessus (cf mon commentaire). Avec la sortie de la RC1 et l’amélioration de la documentation, on commence à y voir plus clair.

Plugin vs Service :

Les plugins sont des extensions, des rajouts. Les services ce sont plutôt l’accès aux « internals ».

  • le cache est un service parce qu’on s’attend toujours au même comportement, quelque soit l’implémentation (base, mémoire, fichier…)
  • le widget est un plugin parce que si ils ont tous une API commune, ils se comportent très différemment les uns les autres

Plugin vs Hook:

Pour faire simple, les plugins remplacent les « hook_info ». L’avantage est double :

  1. regrouper différents hooks au sein d’une même classe. Comme le hook_block_info, le hook_block_view and so on …
  2. disposer grâce à l’héritage d’une implémentation par défaut : on ne code que ce dont on a vraiment besoin.

Notez que les hook_alter en revanche sont conservés.

Mais ou est la liste des plugins alors ? On ne l’a pas sur la page l’API plugin qui se borne à expliquer comment créer/utiliser des plugins.

Il y a une astuce : tous les plugins sont des classes annotées. Il faut aller chercher dans la liste des annotations. Autre solution, installer le module Plugin qui vous fait la liste dans Drupal lui même (mais attention, il faut installer avant le composer_manager sinon vous risquez d’avoir des soucis).

Et maintenant comparons à la liste des hook_info de Drupal 7. Je l’ai remis un peu en forme en deux colonnes pour qu’on voie mieux ce qui a été conservé et ce qui a bougé. Il y a pas mal de différences :

Drupal 8

Drupal 7

Action

hook_action_info

AggregatorFetcher

hook_aggregator_fetch_info

AggregatorParser

hook_aggregator_parse_info

AggregatorProcessor

hook_aggregator_process_info

Archiver

hook_archiver_info

Block

hook_block_info

hook_cron_queue_info

hook_element_info

CKEditorPlugin

Condition

ConfigEntityType

Constraint

ContentEntityType

ContextDefinition

DataType

DisplayVariant

Editor

EntityReferenceSelection

EntityType

hook_entity_info

FieldFormatter

hook_field_formatter_info

FieldType

hook_field_info

hook_field_storage_info

FieldWidget

hook_field_widget_info

conservé

hook_filetransfer_info

Filter

hook_filter_info

conservé

hook_hook_info

FormElement

ImageEffect

hook_image_effect_info

ImageToolkit

ImageToolkitOperation

InPlaceEditor

LanguageNegotiation

hook_language_negotiation_info

conservé

hook_language_types_info

hook_node_info

hook_openid_discovery_method_info

hook_openid_normalization_method_info

Mail

MigrateDestination

MigrateProcessPlugin

MigrateSource

PageDisplayVariant

Plugin

PluginExample

PluginID

QueueWorker

RenderElement

RestResource

SearchPlugin

hook_search_info

hook_system_theme_info

conservé

hook_token_info

hook_trigger_info

conservé

hook_updater_info

module_hook_info

Tip

Translation

ViewsAccess

ViewsArea

ViewsArgument

ViewsArgumentDefault

ViewsArgumentValidator

ViewsCache

ViewsDisplay

ViewsDisplayExtender

ViewsExposedForm

ViewsField

ViewsFilter

ViewsJoin

ViewsPager

,

ViewsQuery

ViewsRelationship

ViewsRow

ViewsSort

ViewsStyle

ViewsWizard

Conclusion :

  • la plupart des hooks_infos sont en effet transformés en Plugins, une poignée a du changer de place (ou ne plus être utile) sauf OpenID qui a été retiré du coeur.
  • Il y a pas mal de petits nouveaux dans Drupal 8, notamment tous ceux qui concernent Views (normal puisque ce module a été intégré au coeur !)
  • Un peu de refactoring coté Entité, ce qui inclus la disparition du hook_node_info (remplacé par l’entity_api. Cf l’exemple du module « example »).

 

Amoureux de Behat

Je viens de passer une semaine à coder des tests behat pour un projet Drupal !

J’en suis tombé amoureux. C’est vraiment l’outil qui manquait pour rédiger des tests : les simpleTest et compagnie, trop complexe à écrire. Cette fois, c’est aussi simple qu’écrire une phrase de base en anglais, et ça marche !

La première fois que j’en ai entendu parler, j’ai négligé Behat parce que je trouvais ça « trop magique pour être vrai ». Et bien non ! Ca fonctionne, et même bien. Et la courbe d’apprentissage est carrément raisonnable. Votre premier test peut s’écrire rapidement, et ensuite vous rentrez dans les subtilités au fur et à mesure des besoins. Le seul bémol peut-être : la doc n’est pas encore très mature.

Combiné à gitlab-ci pour automatiser les tests de non régression lors d’un merge request : que demande le peuple ?

Perl 6 est enfin sorti !!

Ça y est ! Perl 6 est sorti ! Incroyable mais vrai. Et, sur le papier du moins, l’attente en valait la peine.

Je n’ai jamais codé en perl, mais, si j’ai l’occasion je compte bien m’y essayer, essentiellement pour deux raisons :

  • méta-programmation : on peut définir Lua ou Python comme sous langages de « perl6 » !
  • programmation réactive : une fonction est déclenchée automatiquement quand une variable est modifiée

Bien sûr, il y a tout le reste, programmation object/fonctionnelle.

Cela fait des années que je surveille les les langages avec méta-programmation. Les derniers qui m’ont séduits sont Dao et Nim, ainsi que Red.

Red est une reprise de Rebol en open-source. Intéressante mais pas encore mature. Dao serait sympa, mais la communauté est plus que limitée, il ne semble pas prendre son envol. Nim en revanche fait le buzz mais semble un peu complexe, quoi que puissant.

Perl 6 se rajoute donc à la liste. Toute la question est de savoir s’il est facile à apprendre, et … à lire : avoir un langage très puissant c’est intéressant, mais si seul l’expert qui a codé peut relire son code tellement c’est ésotérique ça ne sert pas à grand chose. C’est pour cela d’ailleurs que c++ reste marginal par rapport au C : bien que plus puissant, pour comprendre le code des autres, il faut avoir investit beaucoup de temps. Quand un gars fait des héritages multiples de classes abstraites génériques (templates) on se prends vite la tête 😉

A voir donc si Perl 6 réussi à faire sa place : quand est-ce qu’on recode Drupal en perl ? lol

Drupal 8 Outils du développeur

Comme en témoigne mon petit module hello world, l’API et l’architecture de Drupal 8 sont autrement plus complexe que celle de Drupal7.

Tout programmeur est amené a se poser la question : si je me trompe, comment vais-je debugger mon code ?

Drupal 7 se base sur certains principes fondamentaux simples et universel : les hooks (qui sont des sortes de callbacks améliorées, reproduisant en PHP une forme d’AOP), les tableaux associatifs (et rarement des objets). La plupart des cas sont réglés par un bon vieux « dsm » (du module « devel » qui reste toujours présent dans Drupal 8) qui permet d’afficher le contenu de n’importe quelle variable, façon « krumo » :

Si cette méthode reste valable en Drupal 8, elle peut se révéler trop rudimentaire. Quand on se trompe de nom de classe à hériter ou d’annotation, un dsm ne nous sauvera pas. Il va nous falloir d’autres outils. Je vous en présentes ici les plus utiles.

Composer mananger

Pour l’installer il y a plusieurs étapes :

drush en -y composer_manager
drush composer-manager-init
cd core
composer drupal-update

Permet de lister les packages installés et leurs versions.

Pour rentrer dans les entrailles de la bête :

vim core/vendor/composer/autoload_classmap.php

 

 

Quelques articles sur les problèmes de dépendances (quand 2 composants dépendent de 2 versions différentes d’une même librairie):

Je mentionne le Composer extension pour drush (pas trop compris l’intérêt) : https://drupal.org/project/composer

Petit exemple pour rajouter une dépendance à son module : mymodule/composer.json [attention il faut rajouter composer.json & le champ name est important pour être reconnu par composer manager]

{
  "name": "drupal/mymodule",
  "description": "An automaticcaly generated module",
  "license": "GPL-2.0+",
  "require": {
    "acquia/acquia-sdk-php-cloud-api": ">=0.9.0,<0.10.0",
    "monolog/monolog": "1.10.*"
  }
}

Plugin

Permet de lister les plugins au sens Drupal (eq. hooks regroupés en objets) – ne fonctionne plus à l’heure actuelle en beta12.

Le Web Profiler

Indispensable. C’est un outil qui vient de symfony2 et qui a été adapté à Drupal8.

Pour installer le web-profiler, actuellement (en béta7) il faut suivre plusieurs étapes. En effet, certains modules ne s’installent pas correctement si les dépendances « internes » (packages s2) ne sont pas résolues :

  • Installer drush 7.x
  • Installer le Composer Manager
  • Rafraîchir les dépendances :
    • drush composer-manager-init
    • /core composer drupal-update
  • Installer le WebProfiler

Drupal8 debug1

Le console logger

  • Installer le module « Consoller logger »
  • Lancer la console : drush runserver 2> /dev/null
  • Écrire dans la console : file_put_contents("php://stdout", sprintf("test"));

Debug Twig

  • Activer le debug Twig : sites/default/services.yml
  • {{ dump(text) }}
  • \Doctrine\Common\Util\Debug::dump($votreCollection);

La console Drupal

Semblable à la console symfony2, mais pour Drupal. Permet de générer des modules à la volé, d’avoir un canevas.

Pour l’installer :

curl -LSs http://drupalconsole.com/installer | php

exemple d’utilisation :

./console.phar generate:module

Generator UI

Similaire a la console, mais dédié à la génération de modules. Dispose de plus de templates que la console, mais pourrait être fusionné avec à terme :

Drupal8 - generator UI

Le module upgrader

Permet d’upgrader automatiquement un module drupal 7 en 8. Pour l’installer

drush dl drupalmoduleupgrader

cd drupalmoduleupgrader

composer update

drush en -y drupalmoduleupgrader

 

Transformer un drupal7 mono-site en multi-site avec des sous-répertoires

Voici la manière de transformer un drupal monosite en drupal multisite, sous la forme :

http://www.monsite.fr qui devient http://www.monsite.fr/site1 et http://www.monsite.fr/site2
(et non pas http://site1.monsite.fr et http://site2.monsite.fr – autre cas qui n’est pas traité ici).

1/ La conf d’apache
après le « DocumentRoot » du serveur :
Alias /site1 /var/www/drupalroot
Alias /site2 /var/www/drupalroot

bien entendu, il faut mettre le bon répertoire, celui qui pointe sur la racine de Drupal

2/ le fichier .htaccess dans /var/www/drupalroot
# site1 site
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} ^/site1/(.*)$
RewriteRule ^(.*)$ /site1/index.php [L,QSA]

# site2 site
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} ^/site2/(.*)$
RewriteRule ^(.*)$ /site2/index.php [L,QSA]

# default site
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !=/favicon.ico
RewriteRule ^ index.php [L]

3/ le répertoire « sites »
à la racine de drupal faire:

cd sites
mv default localhost.site1
mkdir localhost.site2
ln -s localhost.site1 site1
ln -s localhost.site2 site2
cd site2
mkdir files
cp ../site1/settings.php
cp ../site1/default.settings.php
chmod 777 files settings.php

cd .  #la racine de Drupal
ln -s . site2
ln -s . site1

4/ le fichier sites.php
ici c’est un peu étrange, mais c’est « Drupal » :  http://www.monsite.fr/site2 et  http://www.monsite.fr/site1 doivent s’écrire  www.monsite.fr.site1 et  www.monsite.fr.site2
On enlève le http, et le / devient un .

<?php
$sites = array(
"www.monsite.fr.site1" => "site1",
"www.monsite.fr.site2" => "site2",
);

changer le base_url dans sites/site1/settings.php
$base_url = ‘https://www.monsite.fr/site1‘;

A ce stade l’ancien site doit fonctionner sous la nouvelle URL, mais le nouveau n’est pas encore prêt

5/ création de la nouvelle base
créer une nouvelle base de donnée vierge : site2_dev

6/ fichiers sites/site2/settings.php

$base_url = ‘https://www.monsite.fr/site2‘;
$databases = array (
'default' =>
array (
'default' =>
array (
'database' => 'site2_dev',

(Attention, dans le cas présent, on est en multibase, d’ou les 2 ‘default’ qui s’enchainent)

7/ installer drupal pour créer la base
https://www.monsite.fr/site2/install.php
suivre les étapes « bêtement ».

Sinon on peut tenter un drush :

drush si ftv_site1_back --notify -y --db-url=mysql://$username$:$password$@localhost/site2_dev --account-mail=admin@local.host --account-name=admin --account-pass=$passwordadmindrupal$

et voila ! normalement à ce stade, vous avez un copie du site fonctionnellement, sans données dedans.

Drupal duel : Drupal 7&8 side by side

Voici, mis cote à cote 2 modules hello-world en Drupal 7 et Drupal 8 (beta9) qui mettent en œuvre les concepts principaux qu’on retrouve dans la plupart des modules :

  • menus et chemins d’accès
  • création de block
  • création d’un formulaire
  • theming
  • accès aux « variables »

Comme vous pouvez le constater, les changements sont nombreux, mais au final :

  • c’est la même chose écrit différement (pour simplifier : on passe du fonctionnel à de l’objet)
  • si on omet les « namespaces », ce n’est pas foncièrement plus verbeux, mais le code est éclaté en 7 fichiers en Drupal8 au lieu de 3
Drupal7 Drupal8

hello.info

hello.info.yml

name = Hello world
description = Minimalist Hello World in Drupal 7
package = Example modules
core = 7.x
files[] = hello.module
name: Hello World
type: module
description: Minimalist Hello World in Drupal 8
package: Example modules
core: 8.x

hello.module

function hello_menu() {

hello.links.menu.yml

$items['helloworld'] = array(
  'title' => 'Hello world',
hello.main:
  title: Hello world
  route_name: hello.world
$items['admin/config/content/hello'] = array(
  'title' => 'Hello config',
hello.form:
  title: Hello config
  route_name: hello.form

hello.routing.yml

$items['helloworld'] = array(
  'page callback' => '_page_hello_world',
  'access callback' => TRUE,
hello.world:
  path: 'helloworld'
  defaults:
    _controller: '\Drupal\hello\Controller\HelloRouteController::index'
  requirements:
    _access: 'TRUE'
$items['admin/config/content/hello'] = array(
  'page callback' => 'drupal_get_form',
  'page arguments' => array('hello_config_form'),
  'access arguments' => array('access hello content')
hello.form:
  path: 'admin/config/content/hello'
  defaults:
    _form: '\Drupal\hello\Form\HelloForm'
  requirements:
    _permission: 'access hello content'
function hello_theme() {
  return array(
    'hello_text' => array(
    'template' => 'hello-text',
    'variables' => array('text' => NULL)),
  );
}
function hello_theme() {
  return array(
    'hello_text' => array(
    'template' => 'hello-text',
    'variables' => array('text' => NULL)),
  );
}

src/Controller/HelloRouteController.php

function _page_hello_world() {
  return array( '#markup' => '<p>Hello world page text</p>' );
}
namespace Drupal\hello\Controller;
use Drupal\Core\Controller\ControllerBase;
class HelloRouteController extends ControllerBase {
  public function index() {
    return array('#markup' => '<p>Hello world page text</p>');
  }
}

src/Plugin/Block/HelloBlock.php

function hello_block_info() {
  $blocks['hello'] = array(
    'info' => t('Hello world block title'),
  );
  return $blocks;
}
namespace Drupal\hello\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\block\Annotation\Block;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Session\AccountInterface;
/**
* Provides a 'Hello' block.
* @Block(
* id = "hello_block",
* admin_label = @Translation("Hello world block title")
* )
*/
function hello_block_view($delta = '') {
  switch ($delta) {
    case 'hello':
      $block['subject'] = t('Hello world');
      $block['content'] = theme('hello_text', array('text' => 
        variable_get('hello_value', 'hello')));
      break;
  }
  return $block;
}
class HelloBlock extends BlockBase {
  public function build() {
    return array('#theme' => 'hello_text', '#text' =>
      \Drupal::config('hello.settings')->get('hello_value'));
  }
}

src/Form/HelloForm.php

function hello_config_form() {
namespace Drupal\hello\Form;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
class HelloForm extends FormBase {
  public function getFormID() {
    return 'hello_form';
  }
  public function buildForm(array $form, 
    FormStateInterface $form_state) {
  $form['hello_config'] = array(
    '#type' => 'textfield',
    '#title' => t('Who are you ?'),
    '#size' => 10,
    '#description' => t('Text for the hello world block.'),
    '#default_value' => 
       variable_get('hello_value', 'anonymous'),
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save'),
  );
  return $form;
}
  $form['hello_config'] = array(
    '#type' => 'textfield',
    '#title' => t('Who are you ?'),
    '#size' => 10,
    '#description' => t('Text for the hello world block.'),
    '#default_value' => 
       \Drupal::config('hello.settings')->get('hello_value'),
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save'),
  );
  return $form;
}
function hello_config_form_submit($form, $form_state) {
  variable_set('hello_value', 
    $form_state['values']['hello_config']);
}
public function submitForm(array &$form, 
  FormStateInterface $form_state) {
  $form_values = $form_state->getValues();
  \Drupal::service('config.factory')->getEditable('hello.settings')
->set('hello_value', $form_values['hello_config'])
->save();
}

drupal7-hello-world/hello-text.tpl.php

Hi from template : <?php print $variables['text']; ?>
Hi from template : {{ text }}