1: <?php
2:
3: namespace Himedia\Padocc;
4:
5: use GAubry\Shell\PathStatus;
6: use GAubry\Shell\ShellAdapter;
7:
8: /**
9: * Collection des propriétés possibles pour un attribut de tâche.
10: * Ces propriétés sont manipulées au sein de champs de bits dans la classe Task.
11: *
12: * @author Geoffroy AUBRY <gaubry@hi-media.com>
13: * @see Task::$aAttrProperties()
14: */
15: class AttributeProperties
16: {
17: /**
18: * Propriété d'attribut : autorise l'utilisation des '${parameter}'.
19: * @var int
20: */
21: const ALLOW_PARAMETER = 1;
22:
23: /**
24: * Propriété d'attribut : l'attribut désigne un répertoire.
25: * @var int
26: */
27: const DIR = 2;
28:
29: /**
30: * Propriété d'attribut : autorise l'utilisation des jokers shell ? et * pour les répertoires
31: * (implique AttributeProperties::DIR).
32: * @var int
33: */
34: const DIRJOKER = 4;
35:
36: /**
37: * Propriété d'attribut : l'attribut désigne un fichier.
38: * @var int
39: */
40: const FILE = 8;
41:
42: /**
43: * Propriété d'attribut : autorise l'utilisation des jokers shell ? et * pour les fichiers
44: * (implique AttributeProperties::FILE).
45: * @var int
46: */
47: const FILEJOKER = 16;
48:
49: /**
50: * Propriété d'attribut : l'attribut est obligatoire.
51: * @var int
52: */
53: const REQUIRED = 32;
54:
55: /**
56: * Propriété d'attribut : l'attribut est un fichier ou un répertoire source et doit donc exister
57: * (implique AttributeProperties::FILE et AttributeProperties::DIR).
58: * @var int
59: */
60: const SRC_PATH = 64;
61:
62: /**
63: * Propriété d'attribut : l'attribut est un booléen sous forme de chaîne de caractères,
64: * valant soit 'true' soit 'false'.
65: * @var int
66: */
67: const BOOLEAN = 128;
68:
69: /**
70: * Propriété d'attribut : l'attribut est une URL.
71: * @var int
72: */
73: const URL = 256;
74:
75: /**
76: * Propriété d'attribut : l'attribut est un email.
77: * @var int
78: */
79: const EMAIL = 512;
80:
81: /**
82: * Propriété d'attribut : l'attribut peut être multi-valué.
83: * @var int
84: */
85: const MULTI_VALUED = 1024;
86:
87: /**
88: * Pattern regex pour scinder les différentes valeurs d'un attribut doté de la propriété MULTI_VALUED.
89: * @var string
90: * @see checkAttributes()
91: */
92: public static $sMultiValuedSep = '/\s*,\s*/';
93:
94: /**
95: * Glue pour concaténer les différentes valeurs d'un attribut doté de la propriété MULTI_VALUED.
96: * @var string
97: * @see checkAttributes()
98: */
99: public static $sMultiValuedJoinGlue = ', ';
100:
101: /**
102: * Shell adapter.
103: * @var ShellAdapter
104: */
105: protected $oShell;
106:
107: /**
108: * Constructeur.
109: *
110: * @param ShellAdapter $oShell
111: */
112: public function __construct (ShellAdapter $oShell)
113: {
114: $this->oShell = $oShell;
115: }
116:
117: /**
118: * Normalise les propriétés des attributs des tâches XML.
119: * Par exemple si c'est un AttributeProperties::FILEJOKER, alors c'est forcément aussi
120: * un AttributeProperties::FILE.
121: *
122: * @param array &$aProperties tableau associatif 'nom d'attribut' => propriétés de l'attribut
123: * @see aAttributeProperties
124: */
125: private function normalizeAttributeProperties (array &$aProperties)
126: {
127: foreach ($aProperties as $sAttribute => $iProperties) {
128: if (($iProperties & self::SRC_PATH) > 0) {
129: $aProperties[$sAttribute] |= self::FILE | self::DIR;
130: }
131: if (($iProperties & self::FILEJOKER) > 0) {
132: $aProperties[$sAttribute] |= self::FILE;
133: }
134: if (($iProperties & self::DIRJOKER) > 0) {
135: $aProperties[$sAttribute] |= self::DIR;
136: }
137: }
138: }
139:
140: /**
141: * Vérifie l'absence d'attribut non permis.
142: *
143: * @param array $aProperties tableau associatif (nom d'attribut => (int)champ de bits de propriétés d'attribut)
144: * Les propriétés sont une combinaisons de constantes de cette classe (ex. self::EMAIL).
145: * @param array $aValues tableau associatif (nom d'attribut => (string)valeur)
146: * @throws \DomainException en cas d'attribut non permis
147: */
148: private function checkUnknownAttributes (array $aProperties, array $aValues)
149: {
150: $aAvailablesAttr = array_keys($aProperties);
151: $aUnknownAttributes = array_diff(array_keys($aValues), $aAvailablesAttr);
152: if (count($aUnknownAttributes) > 0) {
153: throw new \DomainException(
154: "Available attributes: " . print_r($aAvailablesAttr, true)
155: . " => Unknown attribute(s): " . print_r($aUnknownAttributes, true)
156: );
157: }
158: }
159:
160: /**
161: * Vérifie au moyen de tests basiques que les valeurs des attributs sont conformes à leurs propriétés.
162: * Lance une exception si tel n'est pas le cas.
163: *
164: * @param array &$aProperties tableau associatif (nom d'attribut => (int)champ de bits de propriétés d'attribut)
165: * Les propriétés sont une combinaisons de constantes de cette classe (ex. self::EMAIL).
166: * Passé par référence car potentiellement modifié par normalizeAttributeProperties().
167: * @param array &$aValues tableau associatif (nom d'attribut => (string)valeur)
168: * Passé par référence car potentiellement modifié par formatAttribute().
169: * @throws \UnexpectedValueException en cas d'attribut ou fichier manquant
170: * @throws \DomainException en cas d'attribut non permis
171: */
172: public function checkAttributes (array &$aProperties, array &$aValues)
173: {
174: $this->normalizeAttributeProperties($aProperties);
175: $this->checkUnknownAttributes($aProperties, $aValues);
176:
177: foreach ($aProperties as $sName => $iProperties) {
178: if (! empty($aValues[$sName])) {
179: if (($iProperties & self::MULTI_VALUED) > 0) {
180: $aSplittedValues = preg_split(
181: self::$sMultiValuedSep,
182: $aValues[$sName],
183: -1,
184: PREG_SPLIT_NO_EMPTY
185: );
186: } else {
187: $aSplittedValues = array($aValues[$sName]);
188: }
189:
190: foreach ($aSplittedValues as $i => $sSplittedValue) {
191: $aSplittedValues[$i] = $this->formatAttribute($iProperties, $sSplittedValue);
192: $this->checkAttribute($sName, $iProperties, $sSplittedValue);
193: }
194: $aValues[$sName] = implode(self::$sMultiValuedJoinGlue, $aSplittedValues);
195:
196: } else {
197: $this->checkAttribute($sName, $iProperties, '');
198: }
199: }
200: }
201:
202: /**
203: * Formate la valeur d'un attribut au regard de ses propriétés.
204: *
205: * @param int $iProperties champ de bits de propriétés d'attribut,
206: * combinaisons de constantes de cette classe (ex. self::EMAIL).
207: * @param string $sValue valeur de l'attribut
208: * @return string valeur potentiellement formatée de l'attribut au regard de ses propriétés.
209: */
210: private function formatAttribute ($iProperties, $sValue)
211: {
212: if (! empty($sValue)) {
213: if (($iProperties & self::DIR) > 0 || ($iProperties & self::FILE) > 0) {
214: $sValue = str_replace('\\', '/', $sValue);
215: }
216: }
217: return $sValue;
218: }
219:
220: /**
221: * Vérifie au moyen de tests basiques que la valeur de l'attribut spécifié est conforme à ses propriétés.
222: * Lance une exception si tel n'est pas le cas.
223: *
224: * @param string $sName nom d'attribut
225: * @param int $iProperties champ de bits de propriétés d'attribut,
226: * combinaisons de constantes de cette classe (ex. self::EMAIL).
227: * @param string $sValue valeur de l'attribut
228: * @throws \UnexpectedValueException en cas d'attribut ou fichier manquant
229: * @throws \DomainException en cas de valeur non permise
230: */
231: private function checkAttribute ($sName, $iProperties, $sValue)
232: {
233: if (empty($sValue) && ($iProperties & self::REQUIRED) > 0) {
234: throw new \UnexpectedValueException("'$sName' attribute is required!");
235:
236: } elseif (! empty($sValue)) {
237: if (($iProperties & self::BOOLEAN) > 0 && ! in_array($sValue, array('true', 'false'))) {
238: $sMsg = "Value of '$sName' attribute is restricted to 'true' or 'false'. Value: '$sValue'!";
239: throw new \DomainException($sMsg);
240: }
241:
242: if (($iProperties & self::URL) > 0 && preg_match('#^http://#i', $sValue) === 0) {
243: throw new \DomainException("Bad URL: '" . $sValue . "'");
244: }
245:
246: if (($iProperties & self::EMAIL) > 0
247: && preg_match(
248: '#^[[:alnum:]]([-_.]?[[:alnum:]_?])*@[[:alnum:]]([-.]?[[:alnum:]])+\.([a-z]{2,6})$#',
249: $sValue
250: ) === 0
251: ) {
252: throw new \DomainException("Email invalid: '" . $sValue . "'");
253: }
254:
255: if (preg_match('#[*?].*/#', $sValue) !== 0 && ($iProperties & self::DIRJOKER) === 0) {
256: $sMsg = "'*' and '?' jokers are not authorized for directory in '$sName' attribute!";
257: throw new \DomainException($sMsg);
258: }
259:
260: if (preg_match('#[*?](.*[^/])?$#', $sValue) !== 0
261: && ($iProperties & self::FILEJOKER) === 0
262: && ($iProperties & self::URL) === 0
263: ) {
264: $sMsg = "'*' and '?' jokers are not authorized for filename in '$sName' attribute!";
265: throw new \DomainException($sMsg);
266: }
267:
268: if (preg_match('#\$\{[^}]*\}#', $sValue) !== 0 && ($iProperties & self::ALLOW_PARAMETER) === 0) {
269: $sMsg = "Parameters are not allowed in '$sName' attribute! Value: '$sValue'";
270: throw new \DomainException($sMsg);
271: }
272:
273: // Vérification de présence de la source si chemin sans joker ni paramètre :
274: if (($iProperties & self::SRC_PATH) > 0
275: && preg_match('#\*|\?|\$\{[^}]*\}#', $sValue) === 0
276: && $this->oShell->getPathStatus($sValue) === PathStatus::STATUS_NOT_EXISTS
277: ) {
278: throw new \UnexpectedValueException("File or directory '$sValue' not found!");
279: }
280: }
281: }
282: }
283: