Intégration Salesforce
Extension payante. Chaque soumission devient un Lead ou un Contact Salesforce : connexion OAuth à l’organisation, association validée contre le schéma réel, campagne facultative, condition d’envoi, dédoublonnage et journal.
Ce document décrit ce que le module garantit et ce qu’il refuse. Le guide utilisateur en donne la version courte, dans la section « Verser les réponses dans Salesforce ».
Pourquoi ce module est plus strict que les autres
Les organisations qui emploient réellement Salesforce ont des règles de validation, des jeux de permissions, des règles de doublon et des champs personnalisés. Un connecteur permissif y produit des refus en série que personne ne lit, ou pire, des fiches à moitié remplies qu’il faut nettoyer à la main.
Trois partis pris en découlent :
- Les champs proposés viennent de l’organisation, par
describe. Aucune liste écrite d’avance ne décrirait deux organisations à la fois. - Les champs obligatoires sont vérifiés avant l’appel, et nommés dans le journal.
- La référence externe est recommandée, parce qu’elle est la seule chose qui rende une reprise sans danger.
La connexion est au site, l’association est au formulaire
La connexion — une application connectée, un environnement, un couple de jetons — se règle une fois, dans SolisForms → Salesforce.
L’association — l’objet visé, quel champ nourrit quel champ, à quelle condition — se règle par formulaire.
Un formulaire de contact alimente un Lead, un formulaire d’espace client met à
jour un Contact : ce ne sont pas les mêmes champs obligatoires, donc pas la
même association. Un objet choisi au niveau du site aurait imposé le même à tous.
Trois hôtes, et aucun n’est digne de confiance par défaut
L’hôte de connexion est choisi : production (login.salesforce.com), bac à
sable (test.salesforce.com), ou le domaine propre de l’organisation.
Le domaine propre est saisi, donc contrôlé. Une organisation peut vivre sur
monentreprise.my.salesforce.com ou monentreprise--rec.sandbox.my.salesforce.com :
énumérer ces hôtes est impossible, ils contiennent le nom du client. On tient donc
une liste blanche de suffixes — et c’est tout ce qui sépare « l’instance du
client » de « n’importe quelle machine ».
Le piège classique de ce genre de liste est le suffixe sans point :
attaquant-salesforce.com se termine par salesforce.com. Il ne se voit pas à
la lecture du code ; il se voit dans les tests, qui l’essaient explicitement.
L’instance_url est rendue par Salesforce à l’échange de jetons, et c’est
elle qu’on appellera ensuite, avec un jeton d’accès en en-tête. Elle est
contrôlée au même titre, et refusée en HTTP clair.
Les jetons sont chiffrés, et voici ce que cela protège
Le chiffrement est le même que pour Zoho — AES-256-GCM, clé dérivée des sels du site — mais la clé est propre à cette intégration : casser celle de l’une n’ouvre pas les jetons de l’autre.
Protégé : la divulgation de la seule base de données. Injection SQL, sauvegarde égarée, export confié, hébergement mutualisé.
Non protégé : la lecture du système de fichiers. La clé dérive de
wp-config.php ; qui lit ce fichier lit aussi la base.
Sans openssl, rien n’est stocké : la connexion est refusée, avec le motif.
Il n’y a pas de date d’expiration, et c’est la grande différence
Zoho annonce expires_in : on sait quand renouveler. Salesforce ne dit rien — la
durée d’une session dépend d’un réglage de l’organisation, que le site ne lit
pas, et un administrateur peut la révoquer à tout moment.
Le jeton d’accès est donc employé jusqu’à ce qu’il soit refusé, et c’est le
401 INVALID_SESSION_ID qui déclenche le renouvellement — une fois, puis on
s’arrête. Deviner une durée aurait été pire que de ne rien savoir : trop courte,
on épuise le quota ; trop longue, chaque envoi commence par un refus. Le refus
reste de toute façon le seul signal fiable.
Deux portées, pas full
| Portée | Pour |
|---|---|
api | l’accès à l’API REST |
refresh_token | le jeton sans lequel la connexion meurt à la fin de la session |
Ce qui n’est pas demandé compte autant : ni full, ni web, ni chatter_api.
Une portée excédentaire ne se remarque jamais : elle ne change rien tant que rien
ne tourne mal.
Une soumission, un enregistrement
Trois mécanismes se superposent.
La clé unique sur la soumission. Une ligne par soumission dans
slf_salesforce_records : la base refuse la seconde.
La prise de ligne. UPDATE … WHERE state <> 'sent' : celui dont la requête
modifie la ligne envoie, les autres se retirent. Deux tâches planifiées pour la
même soumission se chevauchent dès qu’un envoi dépasse l’intervalle de reprise,
c’est-à-dire précisément quand le CRM est lent. Le contrôle ne peut pas se faire
en PHP : lire l’état puis l’écrire laisse entre les deux l’intervalle où l’autre
processus lit la même valeur.
La référence externe. Reste le cas qu’aucun garde local ne couvre : la requête aboutit chez Salesforce, et la réponse se perd en route. Nous ne savons alors pas si la fiche existe.
Les trois chemins d’écriture, du plus sûr au moins sûr
| Chemin | Requêtes | Survit à une réponse perdue |
|---|---|---|
PATCH par référence externe | 1 | oui |
Recherche par adresse, puis POST ou PATCH | 2 | non |
POST simple | 1 | non |
Le module emprunte le meilleur chemin que le réglage autorise, et l’écran dit ce que les deux derniers ne garantissent pas. La référence est dérivée du site et de la soumission — deux sites qui alimentent la même organisation ont tous deux une soumission numéro 42 — et elle est conservée en base plutôt que recalculée : l’adresse d’un site change, et la fiche déjà posée deviendrait introuvable.
Un champ « External ID » se crée dans Salesforce Setup, en cochant « External ID » sur un champ texte. C’est le seul prérequis côté organisation, et le test de connexion dit s’il en existe un.
La recherche par adresse est paramétrée, pas concaténée
SOQL n’a pas de requêtes préparées, et la valeur cherchée vient d’un formulaire public. Une apostrophe suffirait à sortir de la chaîne. Elles sont échappées, et la valeur bornée. Une injection SOQL ne donne pas l’écriture, mais elle donne la lecture du fichier client.
Les échecs, et lesquels se rejouent
| Motif | Rejoué |
|---|---|
REQUIRED_FIELD_MISSING, INVALID_FIELD, STRING_TOO_LONG | non |
FIELD_CUSTOM_VALIDATION_EXCEPTION (règle de l’organisation) | non |
DUPLICATES_DETECTED (règle de doublon) | non |
INSUFFICIENT_ACCESS_OR_READONLY | non |
UNABLE_TO_LOCK_ROW, 5xx, 429, réseau coupé | oui — 1, 5 puis 30 minutes |
401 INVALID_SESSION_ID | une fois, tout de suite |
invalid_grant à l’échange | non : l’autorisation est perdue |
Les champs obligatoires sont vérifiés avant l’appel, deux fois : à
l’association — Company non associé sur un Lead — et sur les valeurs, parce
qu’un champ facultatif du formulaire associé à Company laisse l’obligation
insatisfaite une fois sur deux.
La campagne ne commande rien
Inscrire la fiche à une campagne est une seconde requête, après l’enregistrement. Elle peut échouer seule : campagne supprimée, droits manquants.
Son échec ne fait pas échouer la livraison, et ne déclenche pas de reprise : la fiche est arrivée, et rejouer pour une campagne supprimée ne la ferait pas revenir. Le détail de la soumission porte une ligne distincte pour la campagne, afin qu’on ne confonde pas « la fiche n’est pas partie » avec « elle n’a pas rejoint la campagne ».
Ce qui ne sort pas
- Les champs non associés. L’envoi est un tableau de correspondances.
- Les mots de passe, paiements, fichiers et signatures, même associés de force dans un réglage importé.
- Les valeurs vides. Une mise à jour qui envoie un champ vide efface la valeur existante : une seconde soumission sans téléphone supprimerait celui que la première avait posé.
Renvoyer
Trois tentatives échouées laissent une soumission au sol. La cause est souvent extérieure et corrigeable : une règle de validation, un champ non associé, un jeu de permissions. Le détail de la soumission porte un bouton de renvoi.
Avec un champ « External ID », renvoyer met à jour la même fiche. Sans, cela peut en créer une seconde — et l’écran le dit plutôt que de le laisser découvrir.
Déconnecter
Le bouton révoque le jeton chez Salesforce avant d’effacer ici. Effacer sans révoquer ne retire rien à l’autorisation : le jeton resterait valable, et une sauvegarde de la base restituerait un accès en état de marche.
Si Salesforce est injoignable, la déconnexion locale se fait quand même, et l’écran dit que l’application reste à retirer à la main.
Diagnostic
L’écran SolisForms → Salesforce porte le décompte des envois par état et les
derniers échecs avec leur motif, écrit tel que Salesforce l’a rendu :
REQUIRED_FIELD_MISSING se cherche dans sa documentation, « champ obligatoire
absent » ne se cherche nulle part.
Le détail de chaque soumission porte le même état, la référence externe, l’état de la campagne, et un lien vers la fiche dans Lightning.
Schéma
Table slf_salesforce_records : une ligne par soumission à envoyer, avec son
objet, son état, son identifiant de fiche, sa référence externe, l’état de la
campagne, le nombre de tentatives et le motif du dernier échec. Aucune valeur de
soumission n’y est recopiée.
La connexion vit dans une option — il n’y en a qu’une par site.
Ce que la V1 ne fait pas
Ni Account, ni Opportunity, ni Case, ni objet personnalisé, ni synchronisation inverse, ni mise à jour de données non associées. Le plan le bornait ainsi, et chacune de ces extensions demande de décider qui a le droit d’écraser quoi dans le système de l’entreprise.
