dolibarr 25.0.0-alpha
PermissionsBlock.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2026 ATM Consulting <support@atm-consulting.fr>
3 * Copyright (C) 2026 MDW <mdeweerd@users.noreply.github.com>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
24require_once DOL_DOCUMENT_ROOT . '/core/lib/functions.lib.php';
25
33{
34 const BEGIN_MARKER = '/* BEGIN MODULEBUILDER PERMISSIONS */';
35 const END_MARKER = '/* END MODULEBUILDER PERMISSIONS */';
36
43 private const ALLOWED_VARIABLES = array('$this', '$r', '$o');
44
52 private const ALLOWED_IDENTIFIERS = array('rights', 'numero', 'sprintf', 'null', 'true', 'false');
53
59 private const ALLOWED_PUNCTUATION = array('[', ']', '(', ')', '=', ';', ',', '.', '+', '-', '*');
60
62 private const CRUD_OFFSETS = array('read' => 0, 'write' => 1, 'delete' => 2);
63
65 private const OBJECT_ID_STRIDE = 10;
66
68 private const INDEX_ID = 0;
69
71 private const INDEX_LABEL = 1;
72
74 private const INDEX_OBJECT = 4;
75
77 private const INDEX_CRUD = 5;
78
80 private const SUPPORTED_INDEXES = array(0, 1, 4, 5);
81
83 private $file;
84
86 private $innerBlock;
87
92 private function __construct(string $file, string $innerBlock)
93 {
94 $this->file = $file;
95 $this->innerBlock = $innerBlock;
96 }
97
105 public static function fromFile(string $file): self
106 {
107 if (strpos($file, '..') !== false) {
108 throw new \RuntimeException('Descriptor path must not contain a parent directory reference: '.$file);
109 }
110 if (!dol_is_file($file)) {
111 throw new \RuntimeException('Descriptor file not found: '.$file);
112 }
113 $content = file_get_contents($file);
114 if ($content === false) {
115 throw new \RuntimeException('Descriptor file is unreadable: '.$file);
116 }
117
118 $posBegin = strpos($content, self::BEGIN_MARKER);
119 $posEnd = strpos($content, self::END_MARKER);
120 if ($posBegin === false || $posEnd === false || $posEnd < $posBegin) {
121 throw new \RuntimeException('Cannot find the start and/or end comments of the permissions section in '.$file);
122 }
123
124 $start = $posBegin + dol_strlen(self::BEGIN_MARKER);
125 $innerBlock = dol_substr($content, $start, $posEnd - $start);
126
127 return new self($file, $innerBlock);
128 }
129
135 public function getInnerBlock(): string
136 {
137 return $this->innerBlock;
138 }
139
150 public function detectTextConflicts(): array
151 {
152 $conflicts = array();
153 $tokens = token_get_all('<?php '.$this->innerBlock);
154
155 foreach ($tokens as $token) {
156 if (is_string($token)) {
157 if (!in_array($token, self::ALLOWED_PUNCTUATION, true)) {
158 $conflicts[] = 'unexpected "'.$token.'" in the permissions block';
159 }
160 continue;
161 }
162
163 list($id, $text, $line) = $token;
164
165 if (in_array($id, array(T_OPEN_TAG, T_WHITESPACE, T_COMMENT, T_DOC_COMMENT, T_INLINE_HTML), true)) {
166 continue;
167 }
168 if (in_array($id, array(T_LNUMBER, T_DNUMBER, T_CONSTANT_ENCAPSED_STRING, T_OBJECT_OPERATOR, T_INC), true)) {
169 continue;
170 }
171 if ($id === T_VARIABLE) {
172 if (!in_array($text, self::ALLOWED_VARIABLES, true)) {
173 $conflicts[] = 'line '.$line.': unexpected variable '.$text.' — the permissions block builds its rights dynamically and cannot be rewritten safely';
174 }
175 continue;
176 }
177 if ($id === T_STRING) {
178 if (!in_array($text, self::ALLOWED_IDENTIFIERS, true)) {
179 $conflicts[] = 'line '.$line.': unexpected identifier "'.$text.'" in the permissions block';
180 }
181 continue;
182 }
183
184 $conflicts[] = 'line '.$line.': unexpected "'.trim($text).'" in the permissions block — the section cannot be rewritten safely';
185 }
186
187 return array_values(array_unique($conflicts));
188 }
189
196 public function detectRightsShapeConflicts(array $permissions): array
197 {
198 $conflicts = array();
199 foreach ($permissions as $i => $right) {
200 if (!is_array($right)) {
201 $conflicts[] = 'right #'.$i.' is not an array';
202 continue;
203 }
204 if (!isset($right[self::INDEX_OBJECT]) || (string) $right[self::INDEX_OBJECT] === '') {
205 $conflicts[] = 'right #'.$i.' declares no object name at index '.self::INDEX_OBJECT;
206 }
207 if (!isset($right[self::INDEX_CRUD]) || (string) $right[self::INDEX_CRUD] === '') {
208 $conflicts[] = 'right #'.$i.' declares no crud code at index '.self::INDEX_CRUD;
209 }
210 }
211
212 return $conflicts;
213 }
214
224 public function detectRightsShapeWarnings(array $permissions): array
225 {
226 $warnings = array();
227 foreach ($permissions as $i => $right) {
228 if (!is_array($right)) {
229 continue;
230 }
231 $unsupported = array_diff(array_keys($right), self::SUPPORTED_INDEXES);
232 if (!empty($unsupported)) {
233 $warnings[] = 'right #'.$i.' carries unsupported index '.implode(', ', $unsupported).', dropped on rewrite';
234 }
235 }
236
237 return $warnings;
238 }
239
250 public function render(array $permissions): string
251 {
252 $grouped = array();
253 foreach ($permissions as $right) {
254 if (!is_array($right) || !isset($right[self::INDEX_OBJECT], $right[self::INDEX_CRUD])) {
255 continue;
256 }
257 $grouped[(string) $right[self::INDEX_OBJECT]][] = $right;
258 }
259
260 $lines = array();
261 $objectIndex = 0;
262 foreach ($grouped as $group) {
263 foreach ($this->assignOffsets($group) as $entry) {
264 $right = $entry['right'];
265 $id = "\$this->numero . sprintf('%02d', (".$objectIndex." * ".self::OBJECT_ID_STRIDE.") + ".$entry['offset']." + 1)";
266
267 $lines[] = "\t\t\$this->rights[\$r][".self::INDEX_ID."] = ".$id.";";
268 $lines[] = "\t\t\$this->rights[\$r][".self::INDEX_LABEL."] = '".$this->escapeForPhpSingleQuotedString((string) ($right[self::INDEX_LABEL] ?? ''))."';";
269 $lines[] = "\t\t\$this->rights[\$r][".self::INDEX_OBJECT."] = '".$this->escapeForPhpSingleQuotedString((string) $right[self::INDEX_OBJECT])."';";
270 $lines[] = "\t\t\$this->rights[\$r][".self::INDEX_CRUD."] = '".$this->escapeForPhpSingleQuotedString((string) $right[self::INDEX_CRUD])."';";
271 $lines[] = "\t\t\$r++;";
272 }
273 $objectIndex++;
274 }
275
276 return empty($lines) ? '' : implode("\n", $lines)."\n";
277 }
278
289 private function assignOffsets(array $group): array
290 {
291 $assigned = array();
292 $usedOffsets = array();
293 $next = count(self::CRUD_OFFSETS);
294
295 foreach ($group as $right) {
296 $crud = (string) $right[self::INDEX_CRUD];
297 if (isset(self::CRUD_OFFSETS[$crud]) && !isset($usedOffsets[self::CRUD_OFFSETS[$crud]])) {
298 $offset = self::CRUD_OFFSETS[$crud];
299 } else {
300 while (isset($usedOffsets[$next])) {
301 $next++;
302 }
303 $offset = $next;
304 }
305 $usedOffsets[$offset] = true;
306 $assigned[] = array('offset' => $offset, 'right' => $right);
307 }
308
309 usort(
310 $assigned,
316 static function (array $a, array $b): int {
317 return $a['offset'] <=> $b['offset'];
318 }
319 );
320
321 return $assigned;
322 }
323
333 private function escapeForPhpSingleQuotedString(string $value): string
334 {
335 return str_replace(array('\\', "'"), array('\\\\', "\\'"), $value);
336 }
337
353 public function write(string $newInnerBlock): int
354 {
355 $pattern = '/'.preg_quote(self::BEGIN_MARKER, '/').'.*?'.preg_quote(self::END_MARKER, '/').'/s';
356 $replacement = self::BEGIN_MARKER."\n".$newInnerBlock."\t\t".self::END_MARKER;
357
358 // preg_replace() reads $1 and \1 in the replacement as backreferences, and a permission
359 // label can legitimately contain either.
360 $replacement = str_replace(array('\\', '$'), array('\\\\', '\\$'), $replacement);
361
362 $result = dolReplaceInFile($this->file, array($pattern => $replacement), '', '0', 0, 1);
363 if ($result <= 0) {
364 dol_syslog('PermissionsBlock::write failed on '.$this->file.' with code '.$result, LOG_ERR);
365 return $result < 0 ? $result : -1;
366 }
367
368 $this->innerBlock = $newInnerBlock;
369
370 return 1;
371 }
372}
write(string $newInnerBlock)
Replace the whole permissions block, markers included, in a single write.
detectRightsShapeWarnings(array $permissions)
List the rights carrying indexes the renderer will drop.
assignOffsets(array $group)
Give each right of one object its numbering offset.
render(array $permissions)
Render a rights array as the new content of the permissions block.
escapeForPhpSingleQuotedString(string $value)
Escape a value for inclusion in a single-quoted PHP string literal.
$id
Support class for third parties, contacts, members, users or resources.
Definition account.php:47
Text engine for the BEGIN/END MODULEBUILDER PERMISSIONS section of a module descriptor.
__construct(string $file, string $innerBlock)
detectRightsShapeConflicts(array $permissions)
List the rights that cannot be rendered at all.
static fromFile(string $file)
Read a descriptor and locate its permissions block.
detectTextConflicts()
List the reasons why rewriting this block would destroy something.
getInnerBlock()
Raw content between the markers, markers excluded.
dol_is_file($pathoffile)
Return if path is a file.
dolReplaceInFile($srcfile, $arrayreplacement, $destfile='', $newmask='0', $indexdatabase=0, $arrayreplacementisregex=0)
Make replacement of strings into a file.
dol_strlen($string, $stringencoding='UTF-8')
Make a strlen call.
dol_substr($string, $start, $length=null, $stringencoding='', $trunconbytes=0)
Make a substring.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.