1<?php 2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project 3// 4// All Rights Reserved. See copyright.txt for details and a complete list of authors. 5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. 6// $Id$ 7 8use Tiki\Package\ComposerManager; 9 10class PreferencesLib 11{ 12 private $data = []; 13 private $usageArray; 14 private $file = ''; 15 private $files = []; 16 // Fake preferences controlled by the system 17 private $system_modified = [ 'tiki_release', 'tiki_version_last_check']; 18 // prefs with system info etc 19 private $system_info = [ 'fgal_use_dir', 'sender_email' ]; 20 21 /** 22 * Returns a list of preferences that can be translated 23 * 24 * @return array List of preferences 25 */ 26 public function getTranslatablePreferences() 27 { 28 global $prefs; 29 30 // Due to performance reasons and the small list of preferences to be translated, returned the current 31 // list of translatable preferences as hardcoded list instead of dynamically searching all preferences 32 /* 33 $translatablePreferences = []; 34 foreach ($prefs as $key => $val) { 35 $definition = $this->getPreference($key); 36 if ($definition['translatable'] === true) { 37 $translatablePreferences[] = $key; 38 } 39 } 40 */ 41 42 $translatablePreferences = [ 43 'browsertitle', 44 'metatag_keywords', 45 'metatag_description', 46 ]; 47 48 return $translatablePreferences; 49 } 50 51 /** 52 * Set the translated value for a given preference 53 * 54 * @param string $pref preference to translate 55 * @param string $lang target language 56 * @param string $val value for the preference 57 * @param string $defaultLanguage the default language 58 */ 59 public function setTranslatedPreference($pref, $lang, $val, $defaultLanguage) 60 { 61 $tikiLib = TikiLib::lib('tiki'); 62 if ($lang != $defaultLanguage) { 63 $pref .= "_" . $lang; 64 } 65 66 if (empty($val)) { 67 $tikiLib->delete_preference($pref); 68 } else { 69 $tikiLib->set_preference($pref, $val); 70 } 71 } 72 73 /** 74 * Retrieve a translated preference, in a given language, or the default if not set 75 * 76 * @param string $name preference name 77 * @param string $lang language to retrieve 78 * @return string translated preference with fallback for the default preference or empty string 79 * @throws Exception 80 */ 81 public function getTranslatedPreference($name, $lang) 82 { 83 global $prefs; 84 85 $translatedPreference = $name; 86 if ($prefs['site_language'] != $lang) { 87 $translatedPreference .= '_' . $lang; 88 } 89 90 if (isset($prefs[$translatedPreference])) { 91 return $prefs[$translatedPreference]; 92 } 93 94 return ''; 95 } 96 97 function getPreference($name, $deps = true, $source = null, $get_pages = false) 98 { 99 global $prefs, $systemConfiguration; 100 static $id = 0; 101 $data = $this->loadData($name); 102 103 if (! isset($data[$name])) { 104 return false; 105 } 106 $defaults = [ 107 'type' => '', 108 'helpurl' => '', 109 'help' => '', 110 'dependencies' => [], 111 'packages_required' => [], 112 'extensions' => [], 113 'dbfeatures' => [], 114 'options' => [], 115 'description' => '', 116 'size' => 40, 117 'detail' => '', 118 'warning' => '', 119 'hint' => '', 120 'shorthint' => '', 121 'perspective' => true, 122 'parameters' => [], 123 'admin' => '', 124 'module' => '', 125 'permission' => '', 126 'plugin' => '', 127 'view' => '', 128 'public' => false, 129 'translatable' => false, 130 ]; 131 if ($data[$name]['type'] === 'textarea') { 132 $defaults['size'] = 10; 133 } 134 135 $info = array_merge($defaults, $data[$name]); 136 137 if ($source == null) { 138 $source = $prefs; 139 } 140 141 $value = isset($source[$name]) ? $source[$name] : null; 142 if (! empty($value) && is_string($value) && (strlen($value) > 1 && $value{1} == ':') && false !== $unserialized = @unserialize($value)) { 143 $value = $unserialized; 144 } 145 146 $info['preference'] = $name; 147 if (isset($info['serialize'])) { 148 $fnc = $info['serialize']; 149 $info['value'] = $fnc($value); 150 } else { 151 $info['value'] = $value; 152 } 153 154 if (! isset($info['tags'])) { 155 $info['tags'] = ['advanced']; 156 } 157 158 $info['tags'][] = $name; 159 $info['tags'][] = 'all'; 160 161 if ($this->checkPreferenceState($info['tags'], 'hide')) { 162 return ['hide' => true]; 163 } 164 165 $info['notes'] = []; 166 167 $info['raw'] = isset($source[$name]) ? $source[$name] : null; 168 $info['id'] = 'pref-' . ++$id; 169 170 if (! empty($info['help']) && isset($prefs['feature_help']) && $prefs['feature_help'] == 'y') { 171 if (preg_match('/^https?:/i', $info['help'])) { 172 $info['helpurl'] = $info['help']; 173 } else { 174 $info['helpurl'] = $prefs['helpurl'] . $info['help']; 175 } 176 } 177 178 /* FIXME: Dependencies are not enforced currently. TODO: Activate disabled code below to enforce dependencies 179 // The value element is deprecated. Use either "configuredValue" or "effectiveValue" instead. 180 $info['configuredValue'] = $info['effectiveValue'] = $info['value']; 181 */ 182 if ($deps && isset($info['dependencies'])) { 183 $info['dependencies'] = $this->getDependencies($info['dependencies']); 184 /* TODO: test 185 if ($info['type'] == 'flag' && 186 $info['effectiveValue'] = 'y' && // Optimization 187 array_filter(array_column($info['dependencies'], 'met'), function($boolean) { 188 return ! $boolean; 189 })) { 190 $info['effectiveValue'] = 'n'; 191 } 192 */ 193 } 194 195 if ($deps && isset($info['packages_required']) && ! empty($info['packages_required'])) { 196 $info['packages_required'] = $this->getPackagesRequired($info['packages_required']); 197 } 198 199 $info['available'] = true; 200 201 if (! $this->checkExtensions($info['extensions'])) { 202 $info['available'] = false; 203 $info['notes'][] = tr('Unmatched system requirement. Missing PHP extension among %0', implode(', ', $info['extensions'])); 204 } 205 206 if (! $this->checkDatabaseFeatures($info['dbfeatures'])) { 207 $info['available'] = false; 208 $info['notes'][] = tr('Unmatched system requirement. The database you are using does not support this feature.'); 209 } 210 211 if (! isset($info['default'])) { // missing default in prefs definition file? 212 $info['modified'] = false; 213 trigger_error(tr('Missing default for preference "%0"', $name), E_USER_WARNING); 214 } else { 215 $info['modified'] = str_replace("\r\n", "\n", $info['value']) != $info['default']; 216 } 217 218 if ($get_pages) { 219 $info['pages'] = $this->getPreferenceLocations($name); 220 } 221 222 if (isset($systemConfiguration->preference->$name)) { 223 $info['available'] = false; 224 $info['notes'][] = tr('Configuration forced by host.'); 225 } 226 227 if ($this->checkPreferenceState($info['tags'], 'deny')) { 228 $info['available'] = false; 229 $info['notes'][] = tr('Disabled by host.'); 230 } 231 232 if (! $info['available']) { 233 $info['tags'][] = 'unavailable'; 234 } 235 236 if ($info['modified'] && $info['available']) { 237 $info['tags'][] = 'modified'; 238 } 239 240 $info['tagstring'] = implode(' ', $info['tags']); 241 242 $info = array_merge($defaults, $info); 243 244 if (! empty($info['permission'])) { 245 if (! isset($info['permission']['show_disabled_features'])) { 246 $info['permission']['show_disabled_features'] = 'y'; 247 } 248 $info['permission'] = 'tiki-objectpermissions.php?' . http_build_query($info['permission'], '', '&'); 249 } 250 251 if (! empty($info['admin'])) { 252 if (preg_match('/^\w+$/', $info['admin'])) { 253 $info['admin'] = 'tiki-admin.php?page=' . urlencode($info['admin']); 254 } 255 } 256 257 if (! empty($info['module'])) { 258 $info['module'] = 'tiki-admin_modules.php?cookietab=3&textFilter=' . urlencode($info['module']); 259 } 260 261 if (! empty($info['plugin'])) { 262 $info['plugin'] = 'tiki-admin.php?page=textarea&cookietab=2&textFilter=' . urlencode($info['plugin']); 263 } 264 265 $smarty = TikiLib::lib('smarty'); 266 $smarty->loadPlugin('smarty_function_icon'); 267 268 if (! empty($info['admin']) || ! empty($info['permission']) || ! empty($info['view']) || ! empty($info['module']) || ! empty($info['plugin'])) { 269 $info['popup_html'] = '<ul class="list-unstyled">'; 270 271 if (! empty($info['admin'])) { 272 $icon = smarty_function_icon([ 'name' => 'settings'], $smarty->getEmptyInternalTemplate()); 273 $info['popup_html'] .= '<li><a class="icon" href="' . $info['admin'] . '">' . $icon . ' ' . tra('Settings') . '</a></li>'; 274 } 275 if (! empty($info['permission'])) { 276 $icon = smarty_function_icon([ 'name' => 'permission'], $smarty->getEmptyInternalTemplate()); 277 $info['popup_html'] .= '<li><a class="icon" href="' . $info['permission'] . '">' . $icon . ' ' . tra('Permissions') . '</a></li>'; 278 } 279 if (! empty($info['view'])) { 280 $icon = smarty_function_icon([ 'name' => 'view'], $smarty->getEmptyInternalTemplate()); 281 $info['popup_html'] .= '<li><a class="icon" href="' . $info['view'] . '">' . $icon . ' ' . tra('View') . '</a></li>'; 282 } 283 if (! empty($info['module'])) { 284 $icon = smarty_function_icon([ 'name' => 'module'], $smarty->getEmptyInternalTemplate()); 285 $info['popup_html'] .= '<li><a class="icon" href="' . $info['module'] . '">' . $icon . ' ' . tra('Modules') . '</a></li>'; 286 } 287 if (! empty($info['plugin'])) { 288 $icon = smarty_function_icon([ 'name' => 'plugin'], $smarty->getEmptyInternalTemplate()); 289 $info['popup_html'] .= '<li><a class="icon" href="' . $info['plugin'] . '">' . $icon . ' ' . tra('Plugins') . '</a></li>'; 290 } 291 $info['popup_html'] .= '</ul>'; 292 } 293 294 if (isset($prefs['connect_feature']) && $prefs['connect_feature'] === 'y') { 295 $connectlib = TikiLib::lib('connect'); 296 $currentVote = $connectlib->getVote($info['preference']); 297 298 $info['voting_html'] = ''; 299 300 if (! in_array('like', $currentVote)) { 301 $info['voting_html'] .= smarty_function_icon($this->getVoteIconParams($info['preference'], 'like', tra('Like')), $smarty->getEmptyInternalTemplate()); 302 } else { 303 $info['voting_html'] .= smarty_function_icon($this->getVoteIconParams($info['preference'], 'unlike', tra("Don't like")), $smarty->getEmptyInternalTemplate()); 304 } 305// if (!in_array('fix', $currentVote)) { 306// $info['voting_html'] .= smarty_function_icon($this->getVoteIconParams($info['preference'], 'fix', tra('Fix me')), $smarty); 307// } else { 308// $info['voting_html'] .= smarty_function_icon($this->getVoteIconParams($info['preference'], 'unfix', tra("Don't fix me")), $smarty); 309// } 310// if (!in_array('wtf', $currentVote)) { 311// $info['voting_html'] .= smarty_function_icon($this->getVoteIconParams($info['preference'], 'wtf', tra("What's this for?")), $smarty); 312// } else { 313// $info['voting_html'] .= smarty_function_icon($this->getVoteIconParams($info['preference'], 'unwtf', tra("What's this for?")), $smarty); 314// } 315 } 316 317 if (! $info['available']) { 318 $info['parameters']['disabled'] = 'disabled'; 319 } 320 321 $info['params'] = ''; 322 if (! empty($info['parameters'])) { 323 foreach ($info['parameters'] as $param => $value) { 324 $info['params'] .= ' ' . $param . '="' . $value . '"'; 325 } 326 } 327 328 /** 329 * If the unified index is enabled, replace simple object selection preferences with object selectors 330 */ 331 if ($info['type'] == 'text' && ! empty($info['profile_reference']) && $prefs['feature_search'] == 'y') { 332 $objectlib = TikiLib::lib('object'); 333 $type = $objectlib->getSelectorType($info['profile_reference']); 334 335 if ($type) { 336 $info['selector_type'] = $type; 337 338 if (empty($info['separator'])) { 339 $info['type'] = 'selector'; 340 } else { 341 $info['type'] = 'multiselector'; 342 } 343 } 344 } 345 346 foreach (['name', 'preference'] as $key) { 347 if (empty($info[$key])) { 348 trigger_error(tr('Missing preference "%0" for "%1"', $key, $name)); 349 } 350 } 351 352 return $info; 353 } 354 355 private function getVoteIconParams($pref, $vote, $label) 356 { 357 $iconname = [ 358 'like' => 'thumbs-up', 359 'unlike' => 'thumbs-down' 360 ]; 361 return [ 362 'name' => $iconname[$vote], 363 'title' => $label, 364 'href' => '#', 'onclick' => 'connectVote(\'' . $pref . '\', \'' . $vote . '\', this);return false;', 365 'class' => '', 366 'iclass' => 'icon connectVoter', 367 'istyle' => 'display:none', 368 ]; 369 } 370 371 /** 372 * Check preference state 373 * @param $tags 374 * @param $state 375 * @return bool 376 */ 377 private function checkPreferenceState($tags, $state) 378 { 379 static $rules = null; 380 381 if (is_null($rules)) { 382 global $systemConfiguration; 383 $rules = $systemConfiguration->rules->toArray(); 384 krsort($rules, SORT_NUMERIC); 385 386 foreach ($rules as & $rule) { 387 $parts = explode(' ', $rule); 388 $type = array_shift($parts); 389 $rule = [$type, $parts]; 390 } 391 } 392 393 394 foreach ($rules as $rule) { 395 $intersect = array_intersect($rule[1], $tags); 396 397 if (count($intersect)) { 398 return $rule[0] == $state; 399 } 400 } 401 402 return false; 403 } 404 405 private function checkExtensions($extensions) 406 { 407 if (count($extensions) == 0) { 408 return true; 409 } 410 411 $installed = get_loaded_extensions(); 412 413 foreach ($extensions as $ext) { 414 if (! in_array($ext, $installed)) { 415 return false; 416 } 417 } 418 419 return true; 420 } 421 422 private function checkDatabaseFeatures($features) 423 { 424 if (in_array('mysql_fulltext', $features)) { 425 return TikiDb::get()->isMySQLFulltextSearchSupported(); 426 } 427 428 return true; 429 } 430 431 /** 432 * Unset hidden preferences based on the configuration file settings 433 * @param $preferences 434 * @return array 435 */ 436 function unsetHiddenPreferences($preferences) 437 { 438 if (empty($preferences)) { 439 return []; 440 } 441 442 foreach ($preferences as $key => $preference) { 443 $preferenceInfo = $this->getPreference($preference); 444 445 if (isset($preferenceInfo['hide']) && $preferenceInfo['hide'] === true) { 446 unset($preferences[$key]); 447 } 448 } 449 450 return $preferences; 451 } 452 453 function getMatchingPreferences($criteria, $filters = null, $maxRecords = 50, $sort = '') 454 { 455 $index = $this->getIndex(); 456 457 $query = new Search_Query($criteria); 458 $query->setCount($maxRecords); 459 460 if ($sort) { 461 $query->setOrder($sort); 462 } 463 if ($filters) { 464 $this->buildPreferenceFilter($query, $filters); 465 } 466 $results = $query->search($index); 467 468 $prefs = []; 469 foreach ($results as $hit) { 470 $prefs[] = $hit['object_id']; 471 } 472 473 return $prefs; 474 } 475 476 /** 477 * @param $handled 478 * @param $data 479 * @param null $limitation 480 * 481 * @return array 482 */ 483 484 function applyChanges($handled, $data, $limitation = null) 485 { 486 global $user_overrider_prefs; 487 $tikilib = TikiLib::lib('tiki'); 488 489 if (is_array($limitation)) { 490 $handled = array_intersect($handled, $limitation); 491 } 492 493 $resets = isset($data['lm_reset']) ? (array) $data['lm_reset'] : []; 494 495 $changes = []; 496 foreach ($handled as $pref) { 497 if (in_array($pref, $resets)) { 498 $tikilib->delete_preference($pref); 499 $changes[$pref] = ['type' => 'reset']; 500 } else { 501 $value = $this->formatPreference($pref, $data); 502 $realPref = in_array($pref, $user_overrider_prefs) ? "site_$pref" : $pref; 503 $old = $this->formatPreference($pref, [$pref => $tikilib->get_preference($realPref)]); 504 if ($old != $value) { 505 if ($tikilib->set_preference($pref, $value)) { 506 $changes[$pref] = ['type' => 'changed', 'new' => $value, 'old' => $old]; 507 } 508 } 509 } 510 } 511 512 return $changes; 513 } 514 515 function formatPreference($pref, $data) 516 { 517 if (false !== $info = $this->getPreference($pref)) { 518 $function = '_get' . ucfirst($info['type']) . 'Value'; 519 $value = $this->$function($info, $data); 520 return $value; 521 } else { 522 if (isset($data[$pref])) { 523 return $data[$pref]; 524 } 525 return null; 526 } 527 } 528 529 function getInput(JitFilter $filter, $preferences = [], $environment = '') 530 { 531 $out = []; 532 533 foreach ($preferences as $name) { 534 $info = $this->getPreference($name); 535 536 if ($environment == 'perspective' && isset($info['perspective']) && $info['perspective'] === false) { 537 continue; 538 } 539 540 if (isset($info['filter'])) { 541 $filter->replaceFilter($name, $info['filter']); 542 } 543 544 if (isset($info['separator'])) { 545 $out[ $name ] = $filter->asArray($name, $info['separator']); 546 } else { 547 $out[ $name ] = $filter[$name]; 548 } 549 } 550 551 return $out; 552 } 553 554 function getExtraSortColumns() 555 { 556 global $prefs; 557 if (isset($prefs['rating_advanced']) && $prefs['rating_advanced'] == 'y') { 558 return TikiDb::get()->fetchMap("SELECT CONCAT('adv_rating_', ratingConfigId), name FROM tiki_rating_configs"); 559 } else { 560 return []; 561 } 562 } 563 564 private function loadData($name) 565 { 566 if (in_array($name, $this->system_modified)) { 567 return null; 568 } 569 if (substr($name, 0, 3) == 'tp_') { 570 $midpos = strpos($name, '__', 3); 571 $pos = strpos($name, '__', $midpos + 2); 572 $file = substr($name, 0, $pos); 573 } elseif (substr($name, 0, 7) == 'themes_') { 574 $pos = strpos($name, '_', 7 + 1); 575 $file = substr($name, 0, $pos); 576 } elseif (false !== $pos = strpos($name, '_')) { 577 $file = substr($name, 0, $pos); 578 } elseif (file_exists(__DIR__ . "/prefs/{$name}.php")) { 579 $file = $name; 580 } else { 581 $file = 'global'; 582 } 583 584 return $this->getFileData($file); 585 } 586 587 private function getFileData($file, $partial = false) 588 { 589 if (! isset($this->files[$file])) { 590 $this->realLoad($file, $partial); 591 } 592 593 $ret = []; 594 if (isset($this->files[$file])) { 595 $ret = $this->files[$file]; 596 } 597 598 if ($partial) { 599 unset($this->files[$file]); 600 } 601 602 return $ret; 603 } 604 605 private function realLoad($file, $partial) 606 { 607 $inc_file = __DIR__ . "/prefs/{$file}.php"; 608 if (substr($file, 0, 3) == "tp_") { 609 $paths = \Tiki\Package\ExtensionManager::getPaths(); 610 $package = str_replace('__', '/', substr($file, 3)); 611 $inc_file = $paths[$package] . "/prefs/{$file}.php"; 612 } 613 if (preg_match('/^themes_(.*)$/', $file, $matches)) { 614 $themeName = $matches[1]; 615 $themePath = TikiLib::lib('theme')->get_theme_path($themeName); 616 $inc_file = $themePath . "prefs/{$file}.php"; 617 } 618 if (file_exists($inc_file)) { 619 require_once $inc_file; 620 $function = "prefs_{$file}_list"; 621 if (function_exists($function)) { 622 $this->files[$file] = $function($partial); 623 } else { 624 $this->files[$file] = []; 625 } 626 } 627 } 628 629 private function getDependencies($dependencies) 630 { 631 $out = []; 632 633 foreach ((array) $dependencies as $key => $dep) { 634 $info = $this->getPreference($dep, false); 635 if ($info) { 636 $out[] = [ 637 'name' => $dep, 638 'label' => $info['name'], 639 'type' => $info['type'], 640 'link' => 'tiki-admin.php?lm_criteria=' . urlencode($info['name']), 641 'met' => 642 ( $info['type'] == 'flag' && $info['value'] == 'y' ) 643 || ( $info['type'] != 'flag' && ! empty($info['value']) ) 644 ]; 645 } elseif ($key == 'profiles') { 646 foreach ((array) $dep as $profile) { 647 $out[] = [ 648 'name' => $profile, 649 'label' => $profile, 650 'type' => 'profile', 651 'link' => 'tiki-admin.php?page=profiles&list=List&profile=' . urlencode($profile), 652 'met' => // FIXME: $info is false, the following surely won't behave as intended. This should indicate whether the profile was applied. 653 ( $info['type'] == 'flag' && $info['value'] == 'y' ) 654 || ( $info['type'] != 'flag' && ! empty($info['value']) ) 655 ]; 656 } 657 } 658 } 659 660 return $out; 661 } 662 663 private function getPackagesRequired($packages) 664 { 665 $out = []; 666 667 foreach ((array) $packages as $key => $dep) { 668 $met = class_exists($dep) || file_exists($dep); 669 670 $package = [ 671 'name' => $key, 672 'label' => $key, 673 'type' => 'composer', 674 'link' => 'tiki-admin.php?page=packages', 675 'met' => $met 676 ]; 677 678 if ($packageInfo = ComposerManager::getPackageInfo($key)) { 679 $package['name'] = $packageInfo['name']; 680 $package['label'] = $packageInfo['name']; 681 682 if (! empty($packageInfo['link'])) { 683 $package['link'] = $packageInfo['link']; 684 } 685 } 686 687 $out[] = $package; 688 } 689 690 return $out; 691 } 692 693 /** 694 * @param bool $fallback Rebuild fallback search index 695 * @return Search_Index_Interface|\ZendSearch\Lucene\SearchIndexInterface|null 696 * @throws Exception 697 */ 698 public function rebuildIndex($fallback = false) 699 { 700 global $prefs; 701 702 $index = TikiLib::lib('unifiedsearch')->getIndex('preference', ! $fallback); 703 $index->destroy(); 704 705 $typeFactory = $index->getTypeFactory(); 706 707 $indexed = []; 708 709 foreach ($this->getAvailableFiles() as $file) { 710 $data = $this->getFileData($file); 711 712 foreach ($data as $pref => $info) { 713 $prefInfo = $this->getPreference($pref); 714 if ($prefInfo) { 715 $info = $prefInfo; 716 } else { 717 $info['preference'] = $pref; 718 if (empty($info['tags'])) { 719 $info['tags'] = ['missing']; 720 } 721 } 722 $doc = $this->indexPreference($typeFactory, $pref, $info); 723 $index->addDocument($doc); 724 725 $indexed[] = $pref; 726 } 727 } 728 729 // Rebuild fallback index 730 list($fallbackEngine) = TikiLib::lib('unifiedsearch')->getFallbackEngineDetails(); 731 if (! $fallback && $fallbackEngine) { 732 $defaultEngine = $prefs['unified_engine']; 733 $prefs['unified_engine'] = $fallbackEngine; 734 $this->rebuildIndex(true); 735 $prefs['unified_engine'] = $defaultEngine; 736 } 737 738 return $index; 739 } 740 741 private function getIndex() 742 { 743 $index = TikiLib::lib('unifiedsearch')->getIndex('preference'); 744 745 if (! $index->exists()) { 746 $index = null; 747 return $this->rebuildIndex(); 748 } 749 750 return $index; 751 } 752 753 function indexNeedsRebuilding() 754 { 755 $index = TikiLib::lib('unifiedsearch')->getIndex('preference'); 756 return ! $index->exists(); 757 } 758 759 public function getPreferenceLocations($name) 760 { 761 if (! $this->usageArray) { 762 $this->loadPreferenceLocations(); 763 } 764 765 $pages = []; 766 foreach ($this->usageArray as $pg => $pfs) { 767 foreach ($pfs as $pf) { 768 if ($pf[0] == $name) { 769 $pages[] = [$pg, $pf[1]]; 770 } 771 } 772 } 773 774 if (strpos($name, 'wikiplugin_') === 0 || strpos($name, 'wikiplugininline_') === 0) { 775 $pages[] = ['textarea', 2]; // plugins are included in textarea admin dynamically 776 } 777 if (strpos($name, 'trackerfield_') === 0) { 778 $pages[] = ['trackers', 3]; // trackerfields are also included in tracker admin dynamically 779 } 780 781 return $pages; 782 } 783 784 private function loadPreferenceLocations() 785 { 786 global $prefs; 787 788 // check for or create array of where each pref is used 789 $file = 'temp/cache/preference-usage-index'; 790 if (! file_exists($file)) { 791 $prefs_usage_array = []; 792 $fp = opendir('templates/admin/'); 793 794 while (false !== ($f = readdir($fp))) { 795 preg_match('/^include_(.*)\.tpl$/', $f, $m); 796 if (count($m) > 0) { 797 $page = $m[1]; 798 $c = file_get_contents('templates/admin/' . $f); 799 preg_match_all('/{preference.*name=[\'"]?(\w*)[\'"]?.*}/i', $c, $m2, PREG_OFFSET_CAPTURE); 800 if (count($m2[1]) > 0) { 801 // count number of tabs in front of each found pref 802 foreach ($m2[1] as & $found) { 803 $tabs = preg_match_all('/{\/tab}/i', substr($c, 0, $found[1]), $m3); 804 if ($tabs === false) { 805 $tabs = 0; 806 } else { 807 $tabs++; 808 } 809 if ($prefs['site_layout'] !== 'classic' && $page === 'look' && $tabs > 2) { 810 $tabs--; // hidden tab #3 for shadow layers 811 } 812 $found[1] = $tabs; // replace char offset with tab number 813 } 814 $prefs_usage_array[$page] = $m2[1]; 815 } 816 } 817 } 818 file_put_contents($file, serialize($prefs_usage_array)); 819 } else { 820 $prefs_usage_array = unserialize(file_get_contents($file)); 821 } 822 823 $this->usageArray = $prefs_usage_array; 824 } 825 826 private function indexPreference($typeFactory, $pref, $info) 827 { 828 $contents = [ 829 $info['preference'], 830 // also index the parts of the pref name individually, e.g. wikiplugin_plugin_name as wikiplugin plugin name 831 str_replace('_', ' ', $info['preference']), 832 $info['name'], 833 isset($info['description']) ? $info['description'] : '', 834 isset($info['keywords']) ? $info['keywords'] : '', 835 ]; 836 837 if (isset($info['options'])) { 838 $contents = array_merge($contents, $info['options']); 839 } 840 841 return [ 842 'object_type' => $typeFactory->identifier('preference'), 843 'object_id' => $typeFactory->identifier($pref), 844 'contents' => $typeFactory->plaintext(implode(' ', $contents)), 845 'tags' => $typeFactory->plaintext(implode(' ', $info['tags'])), 846 ]; 847 } 848 849 private function _getFlagValue($info, $data) 850 { 851 $name = $info['preference']; 852 if (isset($data[$name])&& ! empty($data[$name]) && $data[$name] != 'n') { 853 $ret = 'y'; 854 } else { 855 $ret = 'n'; 856 } 857 858 return $ret; 859 } 860 861 private function _getSelectorValue($info, $data) 862 { 863 $name = $info['preference']; 864 if (! empty($data[$name])) { 865 $value = $data[$name]; 866 867 if (isset($info['filter']) && $filter = TikiFilter::get($info['filter'])) { 868 return $filter->filter($value); 869 } else { 870 return $value; 871 } 872 } 873 } 874 875 private function _getMultiselectorValue($info, $data) 876 { 877 $name = $info['preference']; 878 879 if (isset($data[$name]) && ! empty($data[$name])) { 880 if (! is_array($data[$name])) { 881 $value = explode($info['separator'], $data[$name]); 882 } else { 883 $value = $data[$name]; 884 } 885 } else { 886 $value = []; 887 } 888 889 if (isset($info['filter']) && $filter = TikiFilter::get($info['filter'])) { 890 return array_map([ $filter, 'filter' ], $value); 891 } else { 892 return $value; 893 } 894 } 895 896 private function _getTextValue($info, $data) 897 { 898 $name = $info['preference']; 899 900 if (isset($info['separator']) && is_string($data[$name])) { 901 if (! empty($data[$name])) { 902 $value = explode($info['separator'], $data[$name]); 903 } else { 904 $value = []; 905 } 906 } else { 907 $value = $data[$name]; 908 } 909 910 if (isset($info['filter']) && $filter = TikiFilter::get($info['filter'])) { 911 if (is_array($value)) { 912 $value = array_map([ $filter, 'filter' ], $value); 913 } else { 914 $value = $filter->filter($value); 915 } 916 } 917 return $this->applyConstraints($info, $value); 918 } 919 920 private function _getPasswordValue($info, $data) 921 { 922 $name = $info['preference']; 923 924 if (isset($info['filter']) && $filter = TikiFilter::get($info['filter'])) { 925 return $filter->filter($data[$name]); 926 } else { 927 return $data[$name]; 928 } 929 } 930 931 private function _getTextareaValue($info, $data) 932 { 933 $name = $info['preference']; 934 935 if (isset($info['filter']) && $filter = TikiFilter::get($info['filter'])) { 936 $value = $filter->filter($data[$name]); 937 } else { 938 $value = $data[$name]; 939 } 940 $value = str_replace("\r", "", $value); 941 942 if (isset($info['unserialize'])) { 943 $fnc = $info['unserialize']; 944 945 return $fnc($value); 946 } else { 947 return $value; 948 } 949 } 950 951 private function _getListValue($info, $data) 952 { 953 $name = $info['preference']; 954 $value = isset($data[$name]) ? $data[$name] : null; 955 956 $options = $info['options']; 957 958 if (isset($options[$value])) { 959 return $value; 960 } else { 961 $keys = array_keys($options); 962 return reset($keys); 963 } 964 } 965 966 private function _getMultilistValue($info, $data) 967 { 968 $name = $info['preference']; 969 $value = isset($data[$name]) ? (array) $data[$name] : []; 970 971 $options = $info['options']; 972 $options = array_keys($options); 973 974 return array_intersect($value, $options); 975 } 976 977 private function _getRadioValue($info, $data) 978 { 979 $name = $info['preference']; 980 $value = isset($data[$name]) ? $data[$name] : null; 981 982 $options = $info['options']; 983 $options = array_keys($options); 984 985 if (in_array($value, $options)) { 986 return $value; 987 } else { 988 return ''; 989 } 990 } 991 992 private function _getMulticheckboxValue($info, $data) 993 { 994 return $this->_getMultilistValue($info, $data); 995 } 996 997 /** 998 * Apply constraints (e.g., min or max) defined in the preference info. Currently only used in text type preference. 999 * 1000 * @param $info array preference info from definition 1001 * @param $value mixed value submitted for the preference to be changed to 1002 * @return mixed value preference will be changed to after applying constraints 1003 */ 1004 private function applyConstraints($info, $value) 1005 { 1006 if (isset($info['constraints'])) { 1007 $original = $value; 1008 foreach ($info['constraints'] as $type => $constraint) { 1009 switch ($type) { 1010 case 'min': 1011 if ($value < $constraint) { 1012 $value = $constraint; 1013 Feedback::warning(tr('%0 set to minimum of %1 instead of submitted value of %2', 1014 $info['preference'], $constraint, $original)); 1015 } 1016 break; 1017 case 'max': 1018 if ($value > $constraint) { 1019 $value = $constraint; 1020 Feedback::warning(tr('%0 set to maximum of %1 instead of submitted value of %2', 1021 $info['preference'], $constraint, $original)); 1022 } 1023 break; 1024 } 1025 } 1026 } 1027 return $value; 1028 } 1029 1030 // for export as yaml 1031 1032 /** 1033 * @global TikiLib $tikilib 1034 * @param bool $added shows current prefs not in defaults 1035 * @return array (prefname => array( 'current' => current value, 'default' => default value )) 1036 */ 1037 // NOTE: tikilib contains a similar method called getModifiedPreferences 1038 function getModifiedPrefsForExport($added = false) 1039 { 1040 $tikilib = TikiLib::lib('tiki'); 1041 1042 $prefs = $tikilib->getModifiedPreferences(); 1043 1044 $defaults = get_default_prefs(); 1045 $modified = []; 1046 1047 foreach ($prefs as $pref => $value) { 1048 if (( $added && ! isset($defaults[$pref])) || (isset($defaults[$pref]) && $value !== $defaults[$pref] )) { 1049 if (! in_array($pref, $this->system_modified) && ! in_array($pref, $this->system_info)) { // prefs modified by the system and with system info etc 1050 $preferenceInformation = $this->getPreference($pref); 1051 $modified[$pref] = [ 1052 'current' => ['serial' => $value, 'expanded' => $preferenceInformation['value']], 1053 ]; 1054 if (isset($defaults[$pref])) { 1055 $modified[$pref]['default'] = $defaults[$pref]; 1056 } 1057 } 1058 } 1059 } 1060 ksort($modified); 1061 1062 return $modified; 1063 } 1064 1065 function getDefaults() 1066 { 1067 $defaults = []; 1068 1069 foreach ($this->getAvailableFiles() as $file) { 1070 $data = $this->getFileData($file, true); 1071 1072 foreach ($data as $name => $info) { 1073 if (isset($info['default'])) { 1074 $defaults[$name] = $info['default']; 1075 } else { 1076 $defaults[$name] = ''; 1077 } 1078 } 1079 } 1080 1081 return $defaults; 1082 } 1083 1084 private function getAvailableFiles() 1085 { 1086 $files = []; 1087 foreach (glob(__DIR__ . '/prefs/*.php') as $file) { 1088 if (basename($file) === "index.php") { 1089 continue; 1090 } 1091 $files[] = substr(basename($file), 0, -4); 1092 } 1093 foreach (TikiLib::lib('theme')->get_available_themes() as $theme => $label) { 1094 $themePath = TikiLib::lib('theme')->get_theme_path($theme); 1095 foreach (glob($themePath . 'prefs/*.php') as $file) { 1096 if (basename($file) === "index.php") { 1097 continue; 1098 } 1099 $files[] = substr(basename($file), 0, -4); 1100 } 1101 } 1102 foreach (\Tiki\Package\ExtensionManager::getPaths() as $path) { 1103 foreach (glob($path . '/prefs/*.php') as $file) { 1104 if (basename($file) === "index.php") { 1105 continue; 1106 } 1107 $files[] = substr(basename($file), 0, -4); 1108 } 1109 } 1110 return $files; 1111 } 1112 1113 function setFilters($tags) 1114 { 1115 global $user; 1116 1117 if (! in_array('basic', $tags)) { 1118 $tags[] = 'basic'; 1119 } 1120 TikiLib::lib('tiki')->set_user_preference($user, 'pref_filters', implode(',', $tags)); 1121 } 1122 1123 public function getEnabledFilters() 1124 { 1125 global $user; 1126 $tikilib = TikiLib::lib('tiki'); 1127 $filters = $tikilib->get_user_preference($user, 'pref_filters', 'basic'); 1128 $filters = explode(',', $filters); 1129 return $filters; 1130 } 1131 1132 function getFilters($filters = null) 1133 { 1134 if (! $filters) { 1135 $filters = $this->getEnabledFilters(); 1136 } 1137 1138 $out = [ 1139 'basic' => [ 1140 'label' => tra('Basic'), 1141 'type' => 'positive', 1142 ], 1143 'advanced' => [ 1144 'label' => tra('Advanced'), 1145 'type' => 'positive', 1146 ], 1147 'experimental' => [ 1148 'label' => tra('Experimental'), 1149 'type' => 'negative', 1150 ], 1151 'unavailable' => [ 1152 'label' => tra('Unavailable'), 1153 'type' => 'negative', 1154 ], 1155 'deprecated' => [ 1156 'label' => tra('Deprecated'), 1157 'type' => 'negative', 1158 ], 1159 ]; 1160 1161 foreach ($out as $key => & $info) { 1162 $info['selected'] = in_array($key, $filters); 1163 } 1164 1165 return $out; 1166 } 1167 1168 private function buildPreferenceFilter($query, $input = null) 1169 { 1170 $filters = $this->getFilters($input); 1171 1172 foreach ($filters as $tag => $info) { 1173 if ($info['selected']) { 1174 $positive[] = $tag; 1175 } elseif ($info['type'] == 'negative') { 1176 $query->filterContent("NOT $tag", 'tags'); 1177 } 1178 } 1179 1180 if (count($positive)) { 1181 $query->filterContent(implode(' OR ', $positive), 'tags'); 1182 } 1183 1184 return $query; 1185 } 1186 1187 /*** 1188 * Store 10 most recently changed prefs for quickadmin module menu 1189 * 1190 * @param string $name preference name 1191 * @param string $auser optional user 1192 */ 1193 1194 public function addRecent($name, $auser = null) 1195 { 1196 global $user; 1197 1198 if (! $auser) { 1199 $auser = $user; 1200 } 1201 1202 $list = (array) $this->getRecent($auser); 1203 array_unshift($list, $name); 1204 $list = array_unique($list); 1205 $list = array_slice($list, 0, 10); 1206 1207 TikiLib::lib('tiki')->set_user_preference($auser, 'admin_recent_prefs', serialize($list)); 1208 } 1209 1210 /*** 1211 * Get recent prefs list 1212 * 1213 * @param null $auser option user 1214 * @return array array of pref names 1215 */ 1216 1217 public function getRecent($auser = null) 1218 { 1219 global $user; 1220 $tikilib = TikiLib::lib('tiki'); 1221 1222 if (! $auser) { 1223 $auser = $user; 1224 } 1225 1226 $recent = $tikilib->get_user_preference($auser, 'admin_recent_prefs'); 1227 1228 if (empty($recent)) { 1229 return []; 1230 } else { 1231 return unserialize($recent); 1232 } 1233 } 1234 1235 /** 1236 * Export preferences 1237 * 1238 * @param Tiki_Profile_Writer $writer 1239 * @param string $preferenceName 1240 * @param bool $all 1241 * @return bool 1242 */ 1243 public function exportPreference(Tiki_Profile_Writer $writer, $preferenceName, $all = null) 1244 { 1245 if (isset($preferenceName) && ! $all) { 1246 $listPrefs = []; 1247 $listPrefs[$preferenceName] = true; 1248 } else { 1249 $listPrefs = $this->getModifiedPrefsForExport(true); 1250 } 1251 1252 if (empty($listPrefs)) { 1253 return false; 1254 } 1255 1256 foreach ($listPrefs as $preferenceName => $value) { 1257 if (is_string($preferenceName)) { 1258 if ($info = $this->getPreference($preferenceName)) { 1259 if (isset($info['profile_reference'])) { 1260 $writer->setPreference($preferenceName, $writer->getReference($info['profile_reference'], $info['value'])); 1261 } else { 1262 $writer->setPreference($preferenceName, $info['value']); 1263 } 1264 } 1265 } 1266 } 1267 1268 return true; 1269 } 1270 1271 public function getPackagePrefs() 1272 { 1273 global $prefs; 1274 $ret = []; 1275 foreach (array_keys($prefs) as $prefName) { 1276 if (substr($prefName, 0, 3) == 'tp_') { 1277 $ret[] = $prefName; 1278 } 1279 } 1280 return $ret; 1281 } 1282 1283 /** 1284 * Get a list of preferences that belong to themes 1285 * 1286 * @return array 1287 * @throws Exception 1288 */ 1289 public function getThemePrefs() 1290 { 1291 global $prefs; 1292 $ret = []; 1293 foreach (array_keys($prefs) as $prefName) { 1294 if (substr($prefName, 0, 7) == 'themes_') { 1295 $ret[] = $prefName; 1296 } 1297 } 1298 1299 $themes = TikiLib::lib('theme')->get_available_themes(); 1300 $preferences = []; 1301 foreach ($themes as $key => $theme) { 1302 $themePref = array_filter($ret, function ($pref) use ($key) { 1303 $pattern = '/^themes_' . $key . '_.*/'; 1304 return preg_match($pattern, $pref); 1305 }); 1306 1307 if (! empty($themePref)) { 1308 $preferences[$theme] = $themePref; 1309 } 1310 } 1311 1312 return $preferences; 1313 } 1314 1315 /** 1316 * Filter hidden preferences using an array of preference names 1317 * @return array 1318 */ 1319 public function filterHiddenPreferences($preferences) 1320 { 1321 $hiddenPreferences = []; 1322 1323 if(! empty($preferences)) { 1324 foreach ($preferences as $preference) { 1325 $preferenceDetails = $this->getPreference($preference['name']); 1326 1327 if (! empty($preferenceDetails['hide']) && $preferenceDetails['hide'] === true) { 1328 $hiddenPreferences[] = $preference['name']; 1329 } 1330 } 1331 } 1332 1333 return $hiddenPreferences; 1334 } 1335} 1336