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 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
31{
32 const BEGIN_MARKER = '/* BEGIN MODULEBUILDER PERMISSIONS */';
33 const END_MARKER = '/* END MODULEBUILDER PERMISSIONS */';
34
41 private const ALLOWED_VARIABLES = array('$this', '$r', '$o');
42
50 private const ALLOWED_IDENTIFIERS = array('rights', 'numero', 'sprintf', 'null', 'true', 'false');
51
57 private const ALLOWED_PUNCTUATION = array('[', ']', '(', ')', '=', ';', ',', '.', '+', '-', '*');
58
60 private const CRUD_OFFSETS = array('read' => 0, 'write' => 1, 'delete' => 2);
61
63 private const OBJECT_ID_STRIDE = 10;
64
66 private const INDEX_ID = 0;
67
69 private const INDEX_LABEL = 1;
70
72 private const INDEX_OBJECT = 4;
73
75 private const INDEX_CRUD = 5;
76
78 private const SUPPORTED_INDEXES = array(0, 1, 4, 5);
79
81 private $file;
82
84 private $innerBlock;
85
90 private function __construct(string $file, string $innerBlock)
91 {
92 $this->file = $file;
93 $this->innerBlock = $innerBlock;
94 }
95
103 public static function fromFile(string $file): self
104 {
105 if (strpos($file, '..') !== false) {
106 throw new \RuntimeException('Descriptor path must not contain a parent directory reference: '.$file);
107 }
108 if (!dol_is_file($file)) {
109 throw new \RuntimeException('Descriptor file not found: '.$file);
110 }
111 $content = file_get_contents($file);
112 if ($content === false) {
113 throw new \RuntimeException('Descriptor file is unreadable: '.$file);
114 }
115
116 $posBegin = strpos($content, self::BEGIN_MARKER);
117 $posEnd = strpos($content, self::END_MARKER);
118 if ($posBegin === false || $posEnd === false || $posEnd < $posBegin) {
119 throw new \RuntimeException('Cannot find the start and/or end comments of the permissions section in '.$file);
120 }
121
122 $start = $posBegin + strlen(self::BEGIN_MARKER);
123 $innerBlock = substr($content, $start, $posEnd - $start);
124
125 return new self($file, $innerBlock);
126 }
127
133 public function getInnerBlock(): string
134 {
135 return $this->innerBlock;
136 }
137
148 public function detectTextConflicts(): array
149 {
150 $conflicts = array();
151 $tokens = token_get_all('<?php '.$this->innerBlock);
152
153 foreach ($tokens as $token) {
154 if (is_string($token)) {
155 if (!in_array($token, self::ALLOWED_PUNCTUATION, true)) {
156 $conflicts[] = 'unexpected "'.$token.'" in the permissions block';
157 }
158 continue;
159 }
160
161 list($id, $text, $line) = $token;
162
163 if (in_array($id, array(T_OPEN_TAG, T_WHITESPACE, T_COMMENT, T_DOC_COMMENT, T_INLINE_HTML), true)) {
164 continue;
165 }
166 if (in_array($id, array(T_LNUMBER, T_DNUMBER, T_CONSTANT_ENCAPSED_STRING, T_OBJECT_OPERATOR, T_INC), true)) {
167 continue;
168 }
169 if ($id === T_VARIABLE) {
170 if (!in_array($text, self::ALLOWED_VARIABLES, true)) {
171 $conflicts[] = 'line '.$line.': unexpected variable '.$text.' — the permissions block builds its rights dynamically and cannot be rewritten safely';
172 }
173 continue;
174 }
175 if ($id === T_STRING) {
176 if (!in_array($text, self::ALLOWED_IDENTIFIERS, true)) {
177 $conflicts[] = 'line '.$line.': unexpected identifier "'.$text.'" in the permissions block';
178 }
179 continue;
180 }
181
182 $conflicts[] = 'line '.$line.': unexpected "'.trim($text).'" in the permissions block — the section cannot be rewritten safely';
183 }
184
185 return array_values(array_unique($conflicts));
186 }
187
194 public function detectRightsShapeConflicts(array $permissions): array
195 {
196 $conflicts = array();
197 foreach ($permissions as $i => $right) {
198 if (!is_array($right)) {
199 $conflicts[] = 'right #'.$i.' is not an array';
200 continue;
201 }
202 if (!isset($right[self::INDEX_OBJECT]) || (string) $right[self::INDEX_OBJECT] === '') {
203 $conflicts[] = 'right #'.$i.' declares no object name at index '.self::INDEX_OBJECT;
204 }
205 if (!isset($right[self::INDEX_CRUD]) || (string) $right[self::INDEX_CRUD] === '') {
206 $conflicts[] = 'right #'.$i.' declares no crud code at index '.self::INDEX_CRUD;
207 }
208 }
209
210 return $conflicts;
211 }
212
222 public function detectRightsShapeWarnings(array $permissions): array
223 {
224 $warnings = array();
225 foreach ($permissions as $i => $right) {
226 if (!is_array($right)) {
227 continue;
228 }
229 $unsupported = array_diff(array_keys($right), self::SUPPORTED_INDEXES);
230 if (!empty($unsupported)) {
231 $warnings[] = 'right #'.$i.' carries unsupported index '.implode(', ', $unsupported).', dropped on rewrite';
232 }
233 }
234
235 return $warnings;
236 }
237
248 public function render(array $permissions): string
249 {
250 $grouped = array();
251 foreach ($permissions as $right) {
252 if (!is_array($right) || !isset($right[self::INDEX_OBJECT], $right[self::INDEX_CRUD])) {
253 continue;
254 }
255 $grouped[(string) $right[self::INDEX_OBJECT]][] = $right;
256 }
257
258 $lines = array();
259 $objectIndex = 0;
260 foreach ($grouped as $group) {
261 foreach ($this->assignOffsets($group) as $entry) {
262 $right = $entry['right'];
263 $id = "\$this->numero . sprintf('%02d', (".$objectIndex." * ".self::OBJECT_ID_STRIDE.") + ".$entry['offset']." + 1)";
264
265 $lines[] = "\t\t\$this->rights[\$r][".self::INDEX_ID."] = ".$id.";";
266 $lines[] = "\t\t\$this->rights[\$r][".self::INDEX_LABEL."] = '".$this->escapeForPhpSingleQuotedString((string) ($right[self::INDEX_LABEL] ?? ''))."';";
267 $lines[] = "\t\t\$this->rights[\$r][".self::INDEX_OBJECT."] = '".$this->escapeForPhpSingleQuotedString((string) $right[self::INDEX_OBJECT])."';";
268 $lines[] = "\t\t\$this->rights[\$r][".self::INDEX_CRUD."] = '".$this->escapeForPhpSingleQuotedString((string) $right[self::INDEX_CRUD])."';";
269 $lines[] = "\t\t\$r++;";
270 }
271 $objectIndex++;
272 }
273
274 return empty($lines) ? '' : implode("\n", $lines)."\n";
275 }
276
287 private function assignOffsets(array $group): array
288 {
289 $assigned = array();
290 $usedOffsets = array();
291 $next = count(self::CRUD_OFFSETS);
292
293 foreach ($group as $right) {
294 $crud = (string) $right[self::INDEX_CRUD];
295 if (isset(self::CRUD_OFFSETS[$crud]) && !isset($usedOffsets[self::CRUD_OFFSETS[$crud]])) {
296 $offset = self::CRUD_OFFSETS[$crud];
297 } else {
298 while (isset($usedOffsets[$next])) {
299 $next++;
300 }
301 $offset = $next;
302 }
303 $usedOffsets[$offset] = true;
304 $assigned[] = array('offset' => $offset, 'right' => $right);
305 }
306
307 usort(
308 $assigned,
314 static function (array $a, array $b): int {
315 return $a['offset'] <=> $b['offset'];
316 }
317 );
318
319 return $assigned;
320 }
321
331 private function escapeForPhpSingleQuotedString(string $value): string
332 {
333 return str_replace(array('\\', "'"), array('\\\\', "\\'"), $value);
334 }
335
351 public function write(string $newInnerBlock): int
352 {
353 $pattern = '/'.preg_quote(self::BEGIN_MARKER, '/').'.*?'.preg_quote(self::END_MARKER, '/').'/s';
354 $replacement = self::BEGIN_MARKER."\n".$newInnerBlock."\t\t".self::END_MARKER;
355
356 // preg_replace() reads $1 and \1 in the replacement as backreferences, and a permission
357 // label can legitimately contain either.
358 $replacement = str_replace(array('\\', '$'), array('\\\\', '\\$'), $replacement);
359
360 $result = dolReplaceInFile($this->file, array($pattern => $replacement), '', '0', 0, 1);
361 if ($result <= 0) {
362 dol_syslog('PermissionsBlock::write failed on '.$this->file.' with code '.$result, LOG_ERR);
363 return $result < 0 ? $result : -1;
364 }
365
366 $this->innerBlock = $newInnerBlock;
367
368 return 1;
369 }
370}
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_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
print $langs trans("Show") . '< td style="' . $timeColor . '" align="center"> s</td > badge status0 badge status4 badge status3 Error badge status8< td align="center">< span class="badge ' . $badge . '"></span ></td >< td align="center">< a href="#" class="button button-small" onclick="openLogModal(this)" data-req="' . dol_escape_htmltag($reqSafe) . '" data-res="' . dol_escape_htmltag($resSafe) . '" data-err="' . dol_escape_htmltag($errSafe) . '">< span class="fa fa-search-plus"></span ></a ></td ></tr >< tr >< td colspan="' . $colspan . '" class="opacitymedium"></td ></tr ></table ></div ></form > logModal none logModal none s a JSON string