Activer la compression gzip / deflate sur 1and1 [Astuce]

Ayant un WordPress chez 1and1 j’ai voulu activer la compression de mes pages. Après avoir longuement cherché de partout, j’ai trouvé comment procéder, et ce n’est vraiment pas simple…. En effet, il va falloir faire passer tous nos fichiers (js, css, html, …) par PHP afin qu’ils soient compressés à la volée.

Chemin d’accès complet

Il nous faut le chemin d’accès complet chez 1and1. Pour cela on va mettre un fichier temporaire à la racine de notre site qui se nomme info.php dans lequel on inscrit :

<?php
phpinfo();
?>

On accède à cette page via un navigateur pour avoir toutes les informations liées à PHP. On y cherche la ligne qui correspond à DOCUMENT_ROOT afin de trouver le chemin d’accès complet de notre site ; par exemple /kunden/homepages/25/d3506178849/htdocs/clickandbuilds/WordPress/.

Supprimer le fichier info.php.

Création du prepend file

Maintenant on crée un nouveau fichier à la racine de notre site qui se nomme headers.php dans lequel on met :

<?php
$pathinfo=pathinfo($_ENV['SCRIPT_FILENAME']); 
$extension=$pathinfo['extension']; 
switch ($extension) {
  case 'css':
    $contentType = 'text/css';
    break;
  case 'js':
    $contentType = 'application/x-javascript';
    break;
  case 'xml':
    $contentType = 'text/xml';
    break;
  default:
    $contentType = 'text/html';
    break;
}
header("Content-type: ".$contentType);
// pour vérifier que notre script est bien exécuté
header("X-Homemade-Compression: OK");
?>

Ce fichier va être appelé pour chaque page et va permettre d’envoyer le bon Content-Type.

Fichiers .htaccess et php.ini

Maintenant, cela se complique car il va falloir repérer où se situent toutes les ressources que l’on souhaite compresser. Dans le cas de WordPress, il devrait y avoir :

  • /wp-includes/js/jquery/*
  • /wp-content/themes/votretheme/*

On peut aussi inclure les éventuels répertoires des plugins, et toutes autres ressources.

On se crée un fichier php.ini :

; Server side compression
zlib.output_compression=on
zlib.output_compression_level=9
; tous les fichiers seront parsés par le script headers.php afin d'envoyer le bon Content-Type
auto_prepend_file=/kunden/homepages/25/d3506178849/htdocs/clickandbuilds/WordPress/headers.php

On se crée aussi un fichier .htaccess en y mettant :

AddType x-mapp-php5 .html .htm .css .js .php

Pour que la compression fonctionne sans créer de problème, il va falloir envoyer ces deux fichiers dans chaque répertoire des ressources que l’on souhaite compresser. Par exemple dans /wp-content/themes/votretheme/js/ pour que les fichiers JS du thème soient impactés.

Vous pouvez aussi mettre php.ini à la racine de votre site web, mais sans utiliser le .htaccess de ci-dessus afin d’éviter des problèmes avec l’admin.

On teste

Enfin vous pouvez tester si tout est correct grâce à curl sous Linux :
curl -I -H 'Accept-Encoding: gzip,deflate' --head http://votresite/votreressource.js

Vous devriez voir apparaitre :

X-Homemade-Compression: OK
Content-Encoding: gzip

Gérer les évènements touch sur mobile [JavaScript]

J’ai un projet sur lequel j’ai deux évènements « mouseenter » et « mouseleave » gérés par jQuery (pour faire le « hover »), et je voulais un comportement similaire sur mobile. Pour ce faire, je voulais qu’un « tap » affiche ce que le « hover » affiche avec la souris, puis qu’un second « tap » permette d’accéder au contenu (comme un « clic »). La solution était assez simple, mais j’ai été ensuite confronté au problème du « scroll »…. en effet il confondait le « scroll » avec un « tap ».

Pour empêcher ce comportement, il faut tester si on effectue un « scroll » en s’aidant de $(window).scrollTop():

var scrollPos=0;
$('#elem').on('mouseenter', function() {
  // affiche quelque chose quand la souris est sur la zone
  $(this).addClass('hover');
}).on('mouseleave', function() {
  // cache le contenu lorsque la souris n'est plus dans la zone
  $(this).removeClass('hover');
}).on('touchstart', function(event) {
  // lorsqu'on tape avec son doigt on doit récupérer la position dans la page
  scrollPos=$(window).scrollTop();
}).on('touchend', function(event) {
  event.preventDefault();
  // puis ici on vérifie que ce n'est pas un scroll
  if(Math.abs(scrollPos - $(window).scrollTop()) < 3){
    // si on entre là dedans c'est qu'on a bien effectué un simple "tap"
    var $this=$(this);
    if ($this.hasClass('hover')) {
      // effectue l'action de clic liée à ce bloc ... c'est donc le second "tap"
    } else {
      // effectue la même opération que lorsque la souris est dans la zone
      $this.addClass('hover');
    }
  }
});

Permettre à aux utilisateurs phpBB de prévisualiser les styles du forum

Afin d’offrir une version mobile d’un forum phpbb aux utilisateurs, j’ai cherché comment ils pourraient basculer vers le style/thème mobile du forum juste en ajoutant « ?style=X » dans l’URL.

Normalement cette option n’est offerte qu’à l’administrateur du forum, mais pour l’ouvrir à tous il suffit de modifier le fichier includes/session.php en remplaçant :

if (!empty($_GET['style']) && $auth->acl_get('a_styles') && !defined('ADMIN_START'))

Par :

if (!empty($_GET['style']) && !defined('ADMIN_START'))

Et le tour est joué.

Extraire une image d’un PDF [Astuce]

Pour extraire une image d’un PDF vous pouvez utiliser Photoshop…. ou Gimp ! Ce logiciel gratuit permet également d’extraire une image d’un PDF. Il suffit d’ouvrir le document PDF avec Gimp et il va vous proposer de choisir l’image à extraire.

Simple et efficace.

How to expand Sharepoint 2010 calendar by default [JavaScript]

This code has been tested for Sharepoint 2010 only. It permits to expand by default (so as soon as the page is loaded) the events in the Month calendar view.

See here a solution for Sharepoint 2013.

Tested with IE8 and Firefox 34. You’ll have to add the below JavaScript code into your calendar page:

// the below function simulate a click on a link
function fireEventClick(elem){
    if(document.createEvent){                                                 
      var e = document.createEvent('MouseEvents');
      e.initMouseEvent('click', /* Event type */
      true, /* Can bubble */
      true, /* Cancelable */
      document.defaultView, /* View */
      1, /* Mouse clicks */
      0, /* Screen x */
      0, /* Screen y */
      0, /* Client x */
      0, /* Client y */
      false, /* Ctrl */
      false, /* Alt */
      false, /* Shift */
      false, /* Meta */
      0, /* Button */
      null); /* Related target */
      elem.dispatchEvent(e);                     
    } else { // pour IE
      elem.click();
    }
}

// wait for all the events to be loaded
_spBodyOnLoadFunctionNames.push('changeCalendarEventLinkIntercept');
function changeCalendarEventLinkIntercept() {
  var OldCalendarNotify4a = SP.UI.ApplicationPages.CalendarNotify.$4b;
  SP.UI.ApplicationPages.CalendarNotify.$4b = function () {
    OldCalendarNotify4a();
    // here all the events are loaded so we can expand them
    setTimeout(function() {
      ExpandEvents(0)
    }, 250)
  }
}

// Expand the events
// because Sharepoint redraw ALL the events when we click on Expand, then we need a special recurrent function
function ExpandEvents(idx) {
  var a = document.querySelectorAll('a[evtid="expand_collapse"]');
  if (idx < a.length) {
    if (a[idx].parentNode.getAttribute("_expand") !== "collapse") fireEventClick(a[idx]);
    ExpandEvents(++idx);
  }
}
ExpandEvents(0)

Breadcrumb / steps in CSS that works from IE8 [CSS]

I was looking for a simple code for a step by step process indicator. I found a few things, but everyone seems to have forgotten about IE8… I need to support this old browser at work. So I did my own that I’m sharing here:

  • File Uploaded
  • File Reviewed
  • File Approved

And the code:

<ul class="steps">
  <li class="step complete">File Uploaded</li>
  <li class="arrow">→</li>
  <li class="step current">File Reviewed</li>
  <li class="arrow">→</li>
  <li class="step">File Approved</li>
</ul>
<style>
body {
  counter-reset:steps
}
ul.steps {
  margin:0;
  padding:0;
}
ul.steps li.step {
  background: none repeat scroll 0 0 lightgray;
  border-radius: 1em;
  color: white;
  display: inline-block;
  font-size: 14px;
  height: 4em;
  line-height: 4em;
  margin: 0 1em;
  position: relative;
  text-align: center;
  width: 9em;
}
ul.steps li.step:before {
  content: counter(steps, decimal) ". ";
  counter-increment: steps;
}
ul.steps li.step.complete:before {
  content: "✔ ";
}
ul.steps li.step.complete {
  background-color: green;
}
ul.steps li.step.current {
  background-color:dodgerblue
}
ul.steps li.arrow {
  background: none repeat scroll 0 0 rgba(0, 0, 0, 0);
  color: black;
  display: inline-block;
  font-size: 28px;
  height: 46px;
  line-height: normal;
  margin: 0;
  width: auto;
}
</style>

How to read a remote file and convert it to a Base64 string [JavaScript]

This code is based on the one found on StackOverflow — it’s compatible with IE8+, and all modern browsers:

    // get the remote file binary content
    function getBinary(file, callback) {
      var convertResponseBodyToText = function(e) { return e };
      var xhr = new XMLHttpRequest();  
      xhr.open("GET", file, true);
      if (xhr.overrideMimeType) xhr.overrideMimeType("text/plain; charset=x-user-defined")
      else {
        // for IE8 the binary file are not read correctly, and the only way is to use VBScript
        // see http://stackoverflow.com/questions/1919972/how-do-i-access-xhr-responsebody-for-binary-data-from-javascript-in-ie
        xhr.setRequestHeader("Accept-Charset", "x-user-defined");
        var VB_Fix_IE = '<script language="VBScript">'+"\r\n"
                      + "Function IEBinaryToArray_ByteStr(Binary)\r\n"
                      + "  IEBinaryToArray_ByteStr = CStr(Binary)\r\n"
                      + "End Function\r\n"
                      + "Function IEBinaryToArray_ByteStr_Last(Binary)\r\n"
                      + "  Dim lastIndex\r\n"
                      + "  lastIndex = LenB(Binary)\r\n"
                      + "  if lastIndex mod 2 Then\r\n"
                      + "    IEBinaryToArray_ByteStr_Last = Chr( AscB( MidB( Binary, lastIndex, 1 ) ) )\r\n"
                      + "  Else\r\n"
                      + "    IEBinaryToArray_ByteStr_Last = "+'""'+"\r\n"
                      + "  End If\r\n"
                      + "End Function\r\n"
                      + "\<\/script>\r\n";
        document.write(VB_Fix_IE);
        
        convertResponseBodyToText = function(binary) {
          var byteMapping = {};
          for ( var i = 0; i < 256; i++ ) {
            for ( var j = 0; j < 256; j++ ) {
              byteMapping[ String.fromCharCode( i + j * 256 ) ] = String.fromCharCode(i) + String.fromCharCode(j);
            }
          }
          var rawBytes = IEBinaryToArray_ByteStr(binary);
          var lastChr = IEBinaryToArray_ByteStr_Last(binary);
          return rawBytes.replace(/[\s\S]/g, function( match ) { return byteMapping[match]; }) + lastChr;
        };

      }
      xhr.onreadystatechange = function (aEvt) {
        if (xhr.readyState == 4) {
          if(xhr.status == 200) {
            var data = (xhr.overrideMimeType ? xhr.responseText : convertResponseBodyToText(xhr.responseBody));
            callback(data);
          } else {
            console.log("Cannot find the remote file")
          }
        }
      };
      xhr.send(null);
    }

    // convert the file content to a Base64 string
    function base64Encode(str) {
      var CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
      var out = "", i = 0, len = str.length, c1, c2, c3;
      while (i < len) {
        c1 = str.charCodeAt(i++) & 0xff;
        if (i == len) {
          out += CHARS.charAt(c1 >> 2);
          out += CHARS.charAt((c1 & 0x3) << 4);
          out += "==";
          break;
        }
        c2 = str.charCodeAt(i++);
        if (i == len) {
          out += CHARS.charAt(c1 >> 2);
          out += CHARS.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4));
          out += CHARS.charAt((c2 & 0xF) << 2);
          out += "=";
          break;
        }
        c3 = str.charCodeAt(i++);
        out += CHARS.charAt(c1 >> 2);
        out += CHARS.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
        out += CHARS.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
        out += CHARS.charAt(c3 & 0x3F);
      }
      return out;
    }

    getBinary('http://your.site.com/879258.jpeg', function(binary) {
      console.log(base64Encode(binary))
    })

« En attente d’éléments à copier » et « attente de l’application des modifications » [iOS]

Avec la sortie d’iOS 8 j’ai voulu mettre à jour mon iPad. Malheureusement, au moment de restaurer mes données, iTunes m’indique : « En attente d’éléments à copier » et tourne dans le vide pendant un long moment.

En fait j’ai découvert que cela était dû à des applications qui ne doivent pas être compatibles, ou qui posent problèmes. En l’occurrence celles des chaines françaises D8 et D17. Pour corriger le soucis je suis allé dans l’onglet Apps de mon iPad dans iTunes, puis j’ai supprimé les applications dont je n’avais pas besoin, et celles incriminées. Ensuite il suffit de rappuyer sur le bouton Synchroniser et de vérifier que cette fois ça fonctionne !

Pour tester, vous pouvez retirer toutes les applications, puis voir si ça passe. Si cela ne passe toujours pas, essayez de désactiver les autres synchronisations (comme les chansons, films, images, …) jusqu’à ce que vous trouviez ce qui fait buguer le processus. Si cela vient bien des applications, tentez de les installer une à une…

Problem with the People Picker of Sharepoint

For one of my list I’ve had a very weird issue: one people picker didn’t work properly. If I entered a name and clicked to check it, then it didn’t work. If I saved it and then got back to the form, the value didn’t appear.

This strange behavior is due to a missing SPAN element with _errorLabel as an ID. I have no clue why Sharepoint failed to create this SPAN element… And the error is related to the EntityEditorCallback function that tries to use the SPAN element, but as it doesn’t exist, it returns NULL and fails the function.

To fix it, I’m checking if the SPAN element exists, and if not I create it.
The below code must be called AFTER the line in your document that says :

document.write('<script type="text/javascript" src="/_layouts/entityeditor.js?rev=vXD0hrzDeHYHbfW8aRSMjA%3D%3D"></' + 'script>');

The code to fix this issue is:

// some People Picker are broken for an unknown reason...
// the behavior : we cannot check the name, and the "default" value is not set when editing
// the fix : the span with _errorLabel is not created, so we need to create it
if (typeof EntityEditorCallback === "function") {
  var _EntityEditorCallback = EntityEditorCallback;
  window.EntityEditorCallback = function(result,ctx,preventAutoPostBack) {
    var errorControl=document.getElementById(getSubControlID(ctx, 'errorLabel'));
    // if errorControl doesn't exist then we create it
    if (!errorControl) {
      var e=document.getElementById(ctx);
      var spans=e.getElementsByTagName("td");
      for (var i=0; i<spans.length; i++) {
        if (spans[i].getAttribute("colspan") == 3) {
          spans[i].innerHTML = '';
          break;
        }
      }
    }
    // call the original function
    _EntityEditorCallback(result,ctx,preventAutoPostBack);
  }
}