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 8class Tracker_Field_Wiki extends Tracker_Field_Text implements Tracker_Field_Exportable 9{ 10 public static function getTypes() 11 { 12 global $prefs; 13 if (isset($prefs['tracker_wikirelation_synctitle'])) { 14 $tracker_wikirelation_synctitle = $prefs['tracker_wikirelation_synctitle']; 15 } else { 16 $tracker_wikirelation_synctitle = 'n'; 17 } 18 return [ 19 'wiki' => [ 20 'name' => tr('Wiki Page'), 21 'description' => tr('Embed an associated wiki page'), 22 'help' => 'Wiki page Tracker Field', 23 'prefs' => ['trackerfield_wiki'], 24 'tags' => ['basic'], 25 'default' => 'y', 26 'params' => [ 27 'fieldIdForPagename' => [ 28 'name' => tr('Field that is used for Wiki Page Name'), 29 'description' => tr('Field to get page name to create page name with.'), 30 'filter' => 'int', 31 'profile_reference' => 'tracker_field', 32 'parent' => 'input[name=trackerId]', 33 'parentkey' => 'tracker_id', 34 ], 35 'namespace' => [ 36 'name' => tr('Namespace for Wiki Page'), 37 'description' => tr('The namespace to use for the wiki page to prevent page name clashes. See namespace feature for more information.'), 38 'filter' => 'alpha', 39 'options' => [ 40 'default' => tr('Default (trackerfield<fieldId>)'), 41 'none' => tr('No namespace'), 42 'custom' => tr('Custom namespace'), 43 ], 44 'default' => (isset($prefs['namespace_enabled']) && $prefs['namespace_enabled'] === 'y' ? 'default' : 'none'), 45 ], 46 'customnamespace' => [ 47 'name' => tr('Custom Namespace'), 48 'description' => tr('The custom namespace to use if the custom option is selected.'), 49 'filter' => 'alpha', 50 ], 51 'syncwikipagename' => [ 52 'name' => tr('Rename Wiki Page when changed in tracker'), 53 'description' => tr('Rename associated wiki page when the field that is used for Wiki Page Name is changed.'), 54 'default' => $tracker_wikirelation_synctitle, 55 'filter' => 'alpha', 56 'options' => [ 57 'n' => tr('No'), 58 'y' => tr('Yes'), 59 ], 60 ], 61 'syncwikipagedelete' => [ 62 'name' => tr('Delete Wiki Page when tracker item is deleted'), 63 'description' => tr('Delete associated wiki page when the tracker item is deleted.'), 64 'default' => 'n', 65 'filter' => 'alpha', 66 'options' => [ 67 'n' => tr('No'), 68 'y' => tr('Yes'), 69 ], 70 ], 71 'toolbars' => [ 72 'name' => tr('Toolbars'), 73 'description' => tr('Enable the toolbars as syntax helpers.'), 74 'filter' => 'int', 75 'options' => [ 76 0 => tr('Disable'), 77 1 => tr('Enable'), 78 ], 79 'default' => 1, 80 ], 81 'width' => [ 82 'name' => tr('Width'), 83 'description' => tr('Size of the text area, in characters.'), 84 'filter' => 'int', 85 ], 86 'height' => [ 87 'name' => tr('Height'), 88 'description' => tr('Size of the text area, in lines.'), 89 'filter' => 'int', 90 ], 91 'max' => [ 92 'name' => tr('Character Limit'), 93 'description' => tr('Maximum number of characters to be stored.'), 94 'filter' => 'int', 95 ], 96 'wordmax' => [ 97 'name' => tr('Word Count'), 98 'description' => tr('Limit the length of the text, in number of words.'), 99 'filter' => 'int', 100 ], 101 'wysiwyg' => [ 102 'name' => tr('Use WYSIWYG'), 103 'description' => tr('Use a rich text editor instead of inputting plain text.'), 104 'default' => 'n', 105 'filter' => 'alpha', 106 'options' => [ 107 'n' => tr('No'), 108 'y' => tr('Yes'), 109 ], 110 ], 111 'actions' => [ 112 'name' => tr('Action Buttons'), 113 'description' => tr('Display wiki page buttons when editing the item.'), 114 'default' => 'n', 115 'filter' => 'alpha', 116 'options' => [ 117 'n' => tr('No'), 118 'y' => tr('Yes'), 119 ], 120 ], 121 'samerow' => [ 122 'name' => tr('Same Row'), 123 'description' => tr('Display the field name and input on the same row.'), 124 'deprecated' => false, 125 'filter' => 'int', 126 'default' => 1, 127 'options' => [ 128 0 => tr('No'), 129 1 => tr('Yes'), 130 ], 131 ], 132 'removeBadChars' => [ 133 'name' => tr('Remove Bad Chars'), 134 'description' => tr('Remove bad characters from the Wiki Page name.'), 135 'default' => 'n', 136 'filter' => 'alpha', 137 'options' => [ 138 'n' => tr('No'), 139 'y' => tr('Yes'), 140 ], 141 ], 142 ], 143 ], 144 ]; 145 } 146 147 /** 148 * @param $ins_fields_data 149 * @param int $itemId set to itemId when importing 150 * @return bool 151 */ 152 function isValid($ins_fields_data, $itemId = 0) 153 { 154 global $prefs; 155 156 $pagenameField = $this->getOption('fieldIdForPagename'); 157 $pagename = $this->cleanPageName($ins_fields_data[$pagenameField]['value']); 158 if (! $itemId) { 159 $itemId = $this->getItemId(); 160 } 161 162 if ($this->getOption('namespace') !== 'none' && $prefs['namespace_enabled'] !== 'y') { 163 Feedback::error(tr('Warning: You need to enable the Namespace feature to use the namespace field.')); 164 return false; 165 } 166 167 168 if (TikiLib::lib('trk')->check_field_value_exists($pagename, $pagenameField, $itemId)) { 169 Feedback::error(tr('The page name provided already exists. Please choose another.')); 170 return false; 171 } 172 173 if ($prefs['wiki_badchar_prevent'] == 'y' && TikiLib::lib('wiki')->contains_badchars($pagename)) { 174 $bad_chars = TikiLib::lib('wiki')->get_badchars(); 175 Feedback::error(tr( 176 'The page name specified "%0" contains unallowed characters. It will not be possible to save the page until those are removed: %1', 177 $pagename, 178 $bad_chars 179 )); 180 return false; 181 } 182 183 return true; 184 } 185 186 function getFieldData(array $requestData = []) 187 { 188 $ins_id = $this->getInsertId(); 189 190 global $user, $prefs; 191 192 $to_create_page = false; 193 $page_data = ''; 194 $fieldId = $this->getConfiguration('fieldId'); 195 196 if ($this->getOption('wysiwyg') === 'y' && $prefs['wysiwyg_htmltowiki'] != 'y') { 197 $is_html = true; 198 } else { 199 $is_html = false; 200 } 201 202 $page_name = $this->getValue(); 203 $insForPagenameField = 'ins_' . $this->getOption('fieldIdForPagename'); 204 205 if (! $page_name) { 206 if (! empty($requestData[$insForPagenameField])) { 207 $page_name = $requestData[$insForPagenameField]; // from tabular import replace 208 $itemId = isset($requestData['itemId']) ? $requestData['itemId'] : 0; 209 } else if (! empty($requestData['itemId'])) { 210 $itemData = $this->getItemData(); // calculated field types like auto-increment need rendering 211 $definition = $this->getTrackerDefinition(); 212 $factory = $definition->getFieldFactory(); 213 $field_info = $definition->getField($this->getOption('fieldIdForPagename')); 214 if ($field_info) { 215 $handler = $factory->getHandler($field_info, $itemData); 216 $page_name = $handler->renderOutput(['list_mode' => 'csv']); 217 } else { 218 Feedback::error(tr('Missing Page Name field #%0 for Wiki field #%1', $this->getOption('fieldIdForPagename'), $fieldId)); 219 } 220 $itemId = $requestData['itemId']; 221 } 222 $page_name = $this->getFullPageName($page_name); // from tabular import replace 223 } else { 224 $itemId = $this->getItemId(); 225 } 226 227 if ($page_name) { 228 // There is already a wiki pagename set (the value of the field is the wiki page name) 229 if (TikiLib::lib('tiki')->page_exists($page_name)) { 230 // Get wiki page content 231 $page_info = TikiLib::lib('tiki')->get_page_info($page_name); 232 $page_data = $page_info['data']; 233 if (! empty($requestData[$ins_id])) { 234 // There is new page data provided 235 if ($page_data != $requestData[$ins_id]) { 236 // Update page data 237 $edit_comment = 'Updated by Tracker Field ' . $fieldId; 238 $short_name = $requestData[$insForPagenameField]; 239 $ins_fields_data[$this->getOption('fieldIdForPagename')]['value'] = $short_name; 240 if ($this->isValid($ins_fields_data, $itemId) === true) { 241 TikiLib::lib('tiki')->update_page($page_name, $requestData[$ins_id], $edit_comment, $user, TikiLib::lib('tiki')->get_ip_address(), '', 0, '', $is_html, null, null, $this->getOption('wysiwyg')); 242 } 243 } 244 } 245 } else { 246 $to_create_page = true; 247 } 248 } elseif (! empty($requestData[$ins_id])) { 249 // the field value is currently null and there is input, so would need to create page. 250 if ($short_name = $requestData[$insForPagenameField]) { 251 $page_name = $this->getFullPageName($short_name); 252 if ($page_name && ! TikiLib::lib('tiki')->page_exists($page_name)) { 253 $ins_fields_data[$this->getOption('fieldIdForPagename')]['value'] = $short_name; 254 if ($this->isValid($ins_fields_data) === true) { 255 $to_create_page = true; 256 } 257 } else { 258 Feedback::error(tr('Page "%0" already exists. Not overwriting.', $page_name)); 259 } 260 } 261 } 262 263 if ($to_create_page) { 264 // Note we do not want to create blank pages, but if in the event a page that is already linked is deleted, a blank page will be created. 265 if (! empty($requestData[$ins_id])) { 266 $page_data = $requestData[$ins_id]; 267 } 268 // re-clean the page name here incase it comes from legacy data, i.e. from a partial import 269 $page_name = $this->cleanPageName($page_name); 270 $edit_comment = 'Created by Tracker Field ' . $fieldId; 271 TikiLib::lib('tiki')->create_page($page_name, 0, $page_data, TikiLib::lib('tiki')->now, $edit_comment, $user, TikiLib::lib('tiki')->get_ip_address(), '', '', $is_html, null, $this->getOption('wysiwyg')); 272 } 273 274 if (empty($page_name) && $_SERVER['REQUEST_METHOD'] === 'POST' && empty($requestData[$insForPagenameField])) { 275 // saving a new item may have the wiki page name missing if it is an autoincrement field, so show a warning - TODO better somehow? 276 Feedback::error(tr('Missing Page Name field #%0 value for Wiki field #%1 (so page not created)', $this->getOption('fieldIdForPagename'), $fieldId)); 277 } 278 279 $data = [ 280 'value' => $page_name, 281 'page_data' => $page_data, 282 ]; 283 284 return $data; 285 } 286 287 function renderInput($context = []) 288 { 289 global $prefs; 290 291 static $firstTime = true; 292 293 $cols = $this->getOption('width'); 294 $rows = $this->getOption('height'); 295 296 if ($this->getOption('toolbars') === 0) { 297 $toolbars = false; 298 } else { 299 $toolbars = true; 300 } 301 302 $data = [ 303 'toolbar' => $toolbars ? 'y' : 'n', 304 'cols' => ($cols >= 1) ? $cols : 80, 305 'rows' => ($rows >= 1) ? $rows : 6, 306 'keyup' => '', 307 ]; 308 309 if ($this->getOption('wordmax')) { 310 $data['keyup'] = "wordCount({$this->getOption('wordmax')}, this, 'cpt_{$this->getConfiguration('fieldId')}', '" . addcslashes(tr('Word Limit Exceeded'), "'") . "')"; 311 } elseif ($this->getOption('max')) { 312 $data['keyup'] = "charCount({$this->getOption('max')}, this, 'cpt_{$this->getConfiguration('fieldId')}', '" . addcslashes(tr('Character Limit Exceeded'), "'") . "')"; 313 } 314 $data['element_id'] = 'area_' . uniqid(); 315 if ($firstTime && $this->getOption('wysiwyg') === 'y' && $prefs['wysiwyg_htmltowiki'] != 'y') { // html wysiwyg 316 $is_html = '<input type="hidden" id="allowhtml" value="1" />'; 317 $firstTime = false; 318 } else { 319 $is_html = ''; 320 } 321 $perms = Perms::get(['type' => 'wiki page', 'object' => $this->getValue('')]); 322 $data['perms'] = [ 323 'view' => $perms->view, 324 'edit' => $perms->edit, 325 'wiki_view_source' => $perms->wiki_view_source, 326 'wiki_view_history' => $perms->wiki_view_history, 327 ]; 328 return $this->renderTemplate('trackerinput/wiki.tpl', $context, $data) . $is_html; 329 } 330 331 function renderOutput($context = []) 332 { 333 return $this->attemptParse($this->getConfiguration('page_data')); 334 } 335 336 function getDocumentPart(Search_Type_Factory_Interface $typeFactory) 337 { 338 $data = []; 339 $value = $this->getValue(); 340 $baseKey = $this->getBaseKey(); 341 342 if (! empty($value)) { 343 $info = TikiLib::lib('tiki')->get_page_info($value, true, true); 344 if ($info) { 345 $freshness_days = floor((time() - ($info['lastModif'])) / 86400); 346 $data = [ 347 $baseKey => $typeFactory->identifier($value), 348 "{$baseKey}_text" => $typeFactory->wikitext($info['data']), 349 "{$baseKey}_raw" => $typeFactory->identifier($info['data']), 350 "{$baseKey}_creation_date" => $typeFactory->timestamp($info['created']), 351 "{$baseKey}_modification_date" => $typeFactory->timestamp($info['lastModif']), 352 "{$baseKey}_freshness_days" => $typeFactory->numeric($freshness_days), 353 ]; 354 } 355 } 356 357 return $data; 358 } 359 360 function getProvidedFields() 361 { 362 $baseKey = $this->getBaseKey(); 363 364 $data = [ 365 $baseKey, // the page name 366 "{$baseKey}_text", // wiki text (parsed) 367 "{$baseKey}_raw", // unparsed wiki markup 368 "{$baseKey}_creation_date", // wiki page creation date 369 "{$baseKey}_modification_date", // wiki page modification date 370 "{$baseKey}_freshness_days", // wiki page "freshness" in days 371 ]; 372 373 return $data; 374 } 375 376 function getGlobalFields() 377 { 378 $baseKey = $this->getBaseKey(); 379 380 $data = [ 381 "{$baseKey}_text" => true, 382 ]; 383 384 return $data; 385 } 386 387 function getTabularSchema() 388 { 389 $definition = $this->getTrackerDefinition(); 390 $schema = new Tracker\Tabular\Schema($definition); 391 392 $permName = $this->getConfiguration('permName'); 393 $name = $this->getConfiguration('name'); 394 $insertId = $this->getInsertId(); 395 $baseKey = $this->getBaseKey(); 396 $fieldIdForPagename = $this->getOption('fieldIdForPagename'); 397 $fieldForPagename = $definition->getField($fieldIdForPagename); 398 399 400 $plain = function () { 401 return function ($value, $extra) { 402 if (isset($extra['text'])) { // indexed value from addQuerySource _raw indexed field 403 $value = $extra['text']; 404 } else { 405 // not indexed yet, need to find page contents for $value 406 if (TikiLib::lib('tiki')->page_exists($value)) { 407 // Get wiki page content 408 $page_info = TikiLib::lib('tiki')->get_page_info($value); 409 $value = $page_info['data']; 410 } 411 } 412 413 return $value; 414 }; 415 }; 416 417 $render = function () use ($plain) { 418 $f = $plain(); 419 return function ($value, $extra) use ($f) { 420 $value = $f($value, $extra); 421 422 return $this->attemptParse($value); 423 }; 424 }; 425 426 $schema->addNew($permName, 'default') 427 ->setLabel($name) 428 ->setRenderTransform(function ($value) { 429 return $value; 430 }) 431 ->setParseIntoTransform(function (& $info, $value) use ($permName) { 432 $info['fields'][$permName] = $value; 433 }); 434 435 $schema->addNew($permName, 'content-raw') 436 ->setLabel($name) 437 ->addQuerySource('text', "{$baseKey}_raw") 438 ->setRenderTransform($plain()) 439 ->setParseIntoTransform(function (& $info, $value) use ($permName, $fieldForPagename, $insertId) { 440 $data = $this->getFieldData([ 441 $insertId => $value, 442 'ins_' . $fieldForPagename['fieldId'] => $info['fields'][$fieldForPagename['permName']], 443 'itemId' => empty($info['itemId']) ? 0 : $info['itemId'], 444 ]); 445 $info['fields'][$permName] = $data['value']; 446 }); 447 448 // convert incoming html to wiki syntax and the opposite on export 449 $schema->addNew($permName, 'content-wiki-html') 450 ->setLabel($name) 451 ->addQuerySource('text', "{$baseKey}_raw") 452 ->setRenderTransform($render()) 453 ->setParseIntoTransform(function (& $info, $value) use ($permName, $fieldForPagename, $insertId) { 454 $data = $this->getFieldData([ 455 $this->getInsertId() => TikiLib::lib('edit')->parseToWiki($value), 456 'ins_' . $fieldForPagename['fieldId'] => $info['fields'][$fieldForPagename['permName']], 457 'itemId' => empty($info['itemId']) ? 0 : $info['itemId'], 458 ]); 459 $info['fields'][$permName] = $data['value']; 460 }); 461 462 return $schema; 463 } 464 465 protected function attemptParse($text) 466 { 467 global $prefs; 468 469 $parseOptions = []; 470 if ($this->getOption('wysiwyg') === 'y' && $prefs['wysiwyg_htmltowiki'] != 'y') { 471 $parseOptions['is_html'] = true; 472 } 473 return TikiLib::lib('parser')->parse_data($text, $parseOptions); 474 } 475 476 /** 477 * Gets the full page name including the namespace and separator 478 * 479 * @param $short_name 480 * @return string 481 */ 482 private function getFullPageName($short_name) 483 { 484 global $prefs; 485 486 if (empty($short_name)) { 487 return ''; 488 } 489 490 $namespace = $this->getOption('namespace'); 491 if ($namespace == 'none') { 492 $page_name = $short_name; 493 } elseif ($namespace == 'custom' && ! empty($this->getOption('customnamespace'))) { 494 $page_name = $this->getOption('customnamespace') . $prefs['namespace_separator'] . $short_name; 495 } else { 496 $page_name = 'trackerfield' . $this->getConfiguration('fieldId') . $prefs['namespace_separator'] . $short_name; 497 } 498 499 $page_name = $this->cleanPageName($page_name); 500 501 return $page_name; 502 } 503 504 /** 505 * Gets and cleans the specified page name (i.e. the fieldIdForPagename field value with or without the namespace) 506 * @param $page_name 507 * @return string 508 */ 509 private function cleanPageName($page_name) 510 { 511 $wikilib = TikiLib::lib('wiki'); 512 if ($this->getOption('removeBadChars') === 'y' && $wikilib->contains_badchars($page_name)) { 513 $bad_chars = $wikilib->get_badchars(); 514 $page_name = preg_replace('/[' . preg_quote($bad_chars, '/') . ']/', ' ', $page_name); 515 $page_name = trim(preg_replace('/\s+/', ' ', $page_name)); 516 } 517 if (strlen($page_name) > 160) { 518 $page_name = substr($page_name, 0, 160); 519 } 520 return $page_name; 521 } 522} 523