14731b875SAndreas Gohr<?php 2d6d97f60SAnna Dabrowska 34731b875SAndreas Gohr/** 44731b875SAndreas Gohr * DokuWiki Plugin struct (Action Component) 54731b875SAndreas Gohr * 64731b875SAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 74731b875SAndreas Gohr * @author Andreas Gohr, Michael Große <dokuwiki@cosmocode.de> 84731b875SAndreas Gohr */ 94731b875SAndreas Gohr 107234bfb1Ssplitbrainuse dokuwiki\Extension\ActionPlugin; 117234bfb1Ssplitbrainuse dokuwiki\Extension\EventHandler; 127234bfb1Ssplitbrainuse dokuwiki\Extension\Event; 134ec54c67SAndreas Gohruse dokuwiki\plugin\struct\meta\AccessTable; 14308cc83fSAndreas Gohruse dokuwiki\plugin\struct\meta\AccessTablePage; 154eed39ffSMichael Grosseuse dokuwiki\plugin\struct\meta\Assignments; 1693ca6f4fSAndreas Gohruse dokuwiki\plugin\struct\meta\Column; 174731b875SAndreas Gohruse dokuwiki\plugin\struct\meta\StructException; 1893ca6f4fSAndreas Gohruse dokuwiki\plugin\struct\meta\ValueValidator; 194731b875SAndreas Gohr 204731b875SAndreas Gohr/** 214731b875SAndreas Gohr * Class action_plugin_struct_inline 224731b875SAndreas Gohr * 234731b875SAndreas Gohr * Handle inline editing 244731b875SAndreas Gohr */ 257234bfb1Ssplitbrainclass action_plugin_struct_inline extends ActionPlugin 26d6d97f60SAnna Dabrowska{ 27308cc83fSAndreas Gohr /** @var AccessTablePage */ 287234bfb1Ssplitbrain protected $schemadata; 294731b875SAndreas Gohr 304731b875SAndreas Gohr /** @var Column */ 317234bfb1Ssplitbrain protected $column; 324731b875SAndreas Gohr 334731b875SAndreas Gohr /** @var String */ 344731b875SAndreas Gohr protected $pid = ''; 354731b875SAndreas Gohr 366fd73b4bSAnna Dabrowska /** @var int */ 376fd73b4bSAnna Dabrowska protected $rid = 0; 386fd73b4bSAnna Dabrowska 394731b875SAndreas Gohr /** 404731b875SAndreas Gohr * Registers a callback function for a given event 414731b875SAndreas Gohr * 42*5e29103aSannda * @param EventHandler $controller DokuWiki's event controller object 434731b875SAndreas Gohr * @return void 444731b875SAndreas Gohr */ 457234bfb1Ssplitbrain public function register(EventHandler $controller) 46d6d97f60SAnna Dabrowska { 47748e747fSAnna Dabrowska $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjax'); 484731b875SAndreas Gohr } 494731b875SAndreas Gohr 504731b875SAndreas Gohr /** 51*5e29103aSannda * @param Event $event 524731b875SAndreas Gohr * @param $param 534731b875SAndreas Gohr */ 547234bfb1Ssplitbrain public function handleAjax(Event $event, $param) 55d6d97f60SAnna Dabrowska { 564731b875SAndreas Gohr $len = strlen('plugin_struct_inline_'); 574731b875SAndreas Gohr if (substr($event->data, 0, $len) != 'plugin_struct_inline_') return; 584731b875SAndreas Gohr $event->preventDefault(); 594731b875SAndreas Gohr $event->stopPropagation(); 604731b875SAndreas Gohr 614731b875SAndreas Gohr if (substr($event->data, $len) == 'editor') { 62748e747fSAnna Dabrowska $this->inlineEditor(); 634731b875SAndreas Gohr } 644731b875SAndreas Gohr 654731b875SAndreas Gohr if (substr($event->data, $len) == 'save') { 664731b875SAndreas Gohr try { 67748e747fSAnna Dabrowska $this->inlineSave(); 684731b875SAndreas Gohr } catch (StructException $e) { 694731b875SAndreas Gohr http_status(500); 704731b875SAndreas Gohr header('Content-Type: text/plain; charset=utf-8'); 714731b875SAndreas Gohr echo $e->getMessage(); 724731b875SAndreas Gohr } 734731b875SAndreas Gohr } 74cdd09a96SAndreas Gohr 75cdd09a96SAndreas Gohr if (substr($event->data, $len) == 'cancel') { 76748e747fSAnna Dabrowska $this->inlineCancel(); 77cdd09a96SAndreas Gohr } 784731b875SAndreas Gohr } 794731b875SAndreas Gohr 80cdd09a96SAndreas Gohr /** 81cdd09a96SAndreas Gohr * Creates the inline editor 82cdd09a96SAndreas Gohr */ 83748e747fSAnna Dabrowska protected function inlineEditor() 84d6d97f60SAnna Dabrowska { 85cdd09a96SAndreas Gohr // silently fail when editing not possible 864731b875SAndreas Gohr if (!$this->initFromInput()) return; 876ebbbb8eSAndreas Gohr if (!$this->schemadata->getSchema()->isEditable()) return; 884c3a60d8SAnna Dabrowska // only check page permissions for data with pid, skip for global data 894c3a60d8SAnna Dabrowska if ($this->pid && auth_quickaclcheck($this->pid) < AUTH_EDIT) return; 90cdd09a96SAndreas Gohr if (checklock($this->pid)) return; 914731b875SAndreas Gohr 92cdd09a96SAndreas Gohr // lock page 93cdd09a96SAndreas Gohr lock($this->pid); 944731b875SAndreas Gohr 95cdd09a96SAndreas Gohr // output the editor 964731b875SAndreas Gohr $value = $this->schemadata->getDataColumn($this->column); 97ee983135SMichael Große $id = uniqid('struct__', false); 987c4f397eSRandolf Rotta echo '<div class="field">'; 99ee983135SMichael Große echo '<label data-column="' . hsc($this->column->getFullQualifiedLabel()) . '" for="' . $id . '">'; 10063e019d0SAndreas Gohr echo '</label>'; 1017c4f397eSRandolf Rotta echo '<span class="input">'; 102ee983135SMichael Große echo $value->getValueEditor('entry', $id); 1037c4f397eSRandolf Rotta echo '</span>'; 1044731b875SAndreas Gohr $hint = $this->column->getType()->getTranslatedHint(); 1054731b875SAndreas Gohr if ($hint) { 106712bc832SMichael Große echo '<p class="hint">'; 1074731b875SAndreas Gohr echo hsc($hint); 108712bc832SMichael Große echo '</p>'; 1094731b875SAndreas Gohr } 1107c4f397eSRandolf Rotta echo '</div>'; 111cdd09a96SAndreas Gohr 112cdd09a96SAndreas Gohr // csrf protection 113cdd09a96SAndreas Gohr formSecurityToken(); 1144731b875SAndreas Gohr } 1154731b875SAndreas Gohr 116cdd09a96SAndreas Gohr /** 117cdd09a96SAndreas Gohr * Save the data posted by the inline editor 118cdd09a96SAndreas Gohr */ 119748e747fSAnna Dabrowska protected function inlineSave() 120d6d97f60SAnna Dabrowska { 1214731b875SAndreas Gohr global $INPUT; 1224731b875SAndreas Gohr 12313eddb0fSAndreas Gohr // check preconditions 1244d2da382SAndreas Gohr if (!$this->initFromInput()) { 1254d2da382SAndreas Gohr throw new StructException('inline save error: init'); 1264d2da382SAndreas Gohr } 12713eddb0fSAndreas Gohr self::checkCSRF(); 1280ceefd5cSAnna Dabrowska if (!$this->schemadata->getRid()) { 12913eddb0fSAndreas Gohr $this->checkPage(); 1304eed39ffSMichael Grosse $assignments = Assignments::getInstance(); 1314eed39ffSMichael Grosse $tables = $assignments->getPageAssignments($this->pid, true); 1324eed39ffSMichael Grosse if (!in_array($this->schemadata->getSchema()->getTable(), $tables)) { 1334eed39ffSMichael Grosse throw new StructException('inline save error: schema not assigned to page'); 1344eed39ffSMichael Grosse } 1354731b875SAndreas Gohr } 1366ebbbb8eSAndreas Gohr if (!$this->schemadata->getSchema()->isEditable()) { 1376ebbbb8eSAndreas Gohr throw new StructException('inline save error: no permission for schema'); 1386ebbbb8eSAndreas Gohr } 1394731b875SAndreas Gohr 1404731b875SAndreas Gohr // validate 1414731b875SAndreas Gohr $value = $INPUT->param('entry'); 14293ca6f4fSAndreas Gohr $validator = new ValueValidator(); 1434731b875SAndreas Gohr if (!$validator->validateValue($this->column, $value)) { 1447234bfb1Ssplitbrain throw new StructException(implode("\n", $validator->getErrors())); 1454731b875SAndreas Gohr } 1464731b875SAndreas Gohr 1474731b875SAndreas Gohr // current data 1484731b875SAndreas Gohr $tosave = $this->schemadata->getDataArray(); 1494731b875SAndreas Gohr $tosave[$this->column->getLabel()] = $value; 1504731b875SAndreas Gohr 1514731b875SAndreas Gohr // save 152efe74305SAnna Dabrowska if ($this->schemadata->getRid()) { 15313eddb0fSAndreas Gohr $revision = 0; 15413eddb0fSAndreas Gohr } else { 15513eddb0fSAndreas Gohr $revision = helper_plugin_struct::createPageRevision($this->pid, 'inline edit'); 156858c5caaSMichael Grosse p_get_metadata($this->pid); // reparse the metadata of the page top update the titles/rev/lasteditor table 15713eddb0fSAndreas Gohr } 15813eddb0fSAndreas Gohr $this->schemadata->setTimestamp($revision); 15913eddb0fSAndreas Gohr try { 16013eddb0fSAndreas Gohr if (!$this->schemadata->saveData($tosave)) { 16113eddb0fSAndreas Gohr throw new StructException('saving failed'); 16213eddb0fSAndreas Gohr } 1630ceefd5cSAnna Dabrowska if (!$this->schemadata->getRid()) { 1644eed39ffSMichael Grosse // make sure this schema is assigned 165b8cff1dfSAndreas Gohr /** @noinspection PhpUndefinedVariableInspection */ 1664eed39ffSMichael Grosse $assignments->assignPageSchema( 1674eed39ffSMichael Grosse $this->pid, 1684eed39ffSMichael Grosse $this->schemadata->getSchema()->getTable() 1694eed39ffSMichael Grosse ); 1704eed39ffSMichael Grosse } 171b8cff1dfSAndreas Gohr } catch (\Exception $e) { 172b8cff1dfSAndreas Gohr // PHP <7 needs a catch block 173b8cff1dfSAndreas Gohr throw $e; 17413eddb0fSAndreas Gohr } finally { 17513eddb0fSAndreas Gohr // unlock (unlocking a non-existing file is okay, 17613eddb0fSAndreas Gohr // so we don't check if it's a lookup here 177cdd09a96SAndreas Gohr unlock($this->pid); 17813eddb0fSAndreas Gohr } 1794731b875SAndreas Gohr 1804731b875SAndreas Gohr // reinit then render 18169f7ec8fSAnna Dabrowska $this->initFromInput($this->schemadata->getTimestamp()); 1824731b875SAndreas Gohr $value = $this->schemadata->getDataColumn($this->column); 1834731b875SAndreas Gohr $R = new Doku_Renderer_xhtml(); 1844731b875SAndreas Gohr $value->render($R, 'xhtml'); // FIXME use configured default renderer 185*5e29103aSannda $data = json_encode(['value' => $R->doc, 'rev' => $this->schemadata->getTimestamp()], JSON_THROW_ON_ERROR); 18669f7ec8fSAnna Dabrowska echo $data; 1874731b875SAndreas Gohr } 1884731b875SAndreas Gohr 1894731b875SAndreas Gohr /** 190cdd09a96SAndreas Gohr * Unlock a page (on cancel action) 191cdd09a96SAndreas Gohr */ 192748e747fSAnna Dabrowska protected function inlineCancel() 193d6d97f60SAnna Dabrowska { 194cdd09a96SAndreas Gohr global $INPUT; 195cdd09a96SAndreas Gohr $pid = $INPUT->str('pid'); 196cdd09a96SAndreas Gohr unlock($pid); 197cdd09a96SAndreas Gohr } 198cdd09a96SAndreas Gohr 199cdd09a96SAndreas Gohr /** 2004731b875SAndreas Gohr * Initialize internal state based on input variables 2014731b875SAndreas Gohr * 20269f7ec8fSAnna Dabrowska * @param int $updatedRev timestamp of currently created revision, might be newer than input variable 20369f7ec8fSAnna Dabrowska * @return bool if initialization was successful 2044731b875SAndreas Gohr */ 205d6d97f60SAnna Dabrowska protected function initFromInput($updatedRev = 0) 206d6d97f60SAnna Dabrowska { 2074731b875SAndreas Gohr global $INPUT; 2084731b875SAndreas Gohr 2094731b875SAndreas Gohr $this->schemadata = null; 2104731b875SAndreas Gohr $this->column = null; 2114731b875SAndreas Gohr 2124731b875SAndreas Gohr $pid = $INPUT->str('pid'); 2136fd73b4bSAnna Dabrowska $rid = $INPUT->int('rid'); 21469f7ec8fSAnna Dabrowska $rev = $updatedRev ?: $INPUT->int('rev'); 21569f7ec8fSAnna Dabrowska 2167234bfb1Ssplitbrain [$table, $field] = explode('.', $INPUT->str('field'), 2); 2176fd73b4bSAnna Dabrowska if (blank($pid) && blank($rid)) return false; 2184731b875SAndreas Gohr if (blank($table)) return false; 2194731b875SAndreas Gohr if (blank($field)) return false; 2204731b875SAndreas Gohr 2214731b875SAndreas Gohr $this->pid = $pid; 2224ec54c67SAndreas Gohr try { 2234cd5cc28SAnna Dabrowska if (AccessTable::isTypePage($pid, $rev)) { 2244cd5cc28SAnna Dabrowska $this->schemadata = AccessTable::getPageAccess($table, $pid); 2254cd5cc28SAnna Dabrowska } elseif (AccessTable::isTypeSerial($pid, $rev)) { 2264cd5cc28SAnna Dabrowska $this->schemadata = AccessTable::getSerialAccess($table, $pid, $rid); 2274cd5cc28SAnna Dabrowska } else { 228308cc83fSAndreas Gohr $this->schemadata = AccessTable::getGlobalAccess($table, $rid); 2294cd5cc28SAnna Dabrowska } 2304ec54c67SAndreas Gohr } catch (StructException $ignore) { 2314731b875SAndreas Gohr return false; 2324731b875SAndreas Gohr } 2334731b875SAndreas Gohr 2344ec54c67SAndreas Gohr $this->column = $this->schemadata->getSchema()->findColumn($field); 2354731b875SAndreas Gohr if (!$this->column || !$this->column->isVisibleInEditor()) { 2364731b875SAndreas Gohr $this->schemadata = null; 2374731b875SAndreas Gohr $this->column = null; 2384731b875SAndreas Gohr return false; 2394731b875SAndreas Gohr } 2404731b875SAndreas Gohr 2414731b875SAndreas Gohr return true; 2424731b875SAndreas Gohr } 2434731b875SAndreas Gohr 24413eddb0fSAndreas Gohr /** 24513eddb0fSAndreas Gohr * Checks if a page can be edited 24613eddb0fSAndreas Gohr * 24713eddb0fSAndreas Gohr * @throws StructException when check fails 24813eddb0fSAndreas Gohr */ 249d6d97f60SAnna Dabrowska protected function checkPage() 250d6d97f60SAnna Dabrowska { 25113eddb0fSAndreas Gohr if (!page_exists($this->pid)) { 25213eddb0fSAndreas Gohr throw new StructException('inline save error: no such page'); 25313eddb0fSAndreas Gohr } 25413eddb0fSAndreas Gohr if (auth_quickaclcheck($this->pid) < AUTH_EDIT) { 25513eddb0fSAndreas Gohr throw new StructException('inline save error: acl'); 25613eddb0fSAndreas Gohr } 25713eddb0fSAndreas Gohr if (checklock($this->pid)) { 25813eddb0fSAndreas Gohr throw new StructException('inline save error: lock'); 25913eddb0fSAndreas Gohr } 26013eddb0fSAndreas Gohr } 26113eddb0fSAndreas Gohr 26213eddb0fSAndreas Gohr /** 26313eddb0fSAndreas Gohr * Our own implementation of checkSecurityToken because we don't want the msg() call 26413eddb0fSAndreas Gohr * 26513eddb0fSAndreas Gohr * @throws StructException when check fails 26613eddb0fSAndreas Gohr */ 267d6d97f60SAnna Dabrowska public static function checkCSRF() 268d6d97f60SAnna Dabrowska { 26913eddb0fSAndreas Gohr global $INPUT; 27013eddb0fSAndreas Gohr if ( 27113eddb0fSAndreas Gohr $INPUT->server->str('REMOTE_USER') && 27213eddb0fSAndreas Gohr getSecurityToken() != $INPUT->str('sectok') 27313eddb0fSAndreas Gohr ) { 27413eddb0fSAndreas Gohr throw new StructException('CSRF check failed'); 27513eddb0fSAndreas Gohr } 27613eddb0fSAndreas Gohr } 2774731b875SAndreas Gohr} 278