14731b875SAndreas Gohr<?php 24731b875SAndreas Gohr/** 34731b875SAndreas Gohr * DokuWiki Plugin struct (Action Component) 44731b875SAndreas Gohr * 54731b875SAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 64731b875SAndreas Gohr * @author Andreas Gohr, Michael Große <dokuwiki@cosmocode.de> 74731b875SAndreas Gohr */ 84731b875SAndreas Gohr 94731b875SAndreas Gohr// must be run within Dokuwiki 104ec54c67SAndreas Gohruse dokuwiki\plugin\struct\meta\AccessTable; 1194c9aa4cSAndreas Gohruse dokuwiki\plugin\struct\meta\AccessTableData; 124eed39ffSMichael Grosseuse dokuwiki\plugin\struct\meta\Assignments; 1393ca6f4fSAndreas Gohruse dokuwiki\plugin\struct\meta\Column; 144731b875SAndreas Gohruse dokuwiki\plugin\struct\meta\StructException; 1593ca6f4fSAndreas Gohruse dokuwiki\plugin\struct\meta\ValueValidator; 164731b875SAndreas Gohr 174731b875SAndreas Gohrif(!defined('DOKU_INC')) die(); 184731b875SAndreas Gohr 194731b875SAndreas Gohr/** 204731b875SAndreas Gohr * Class action_plugin_struct_inline 214731b875SAndreas Gohr * 224731b875SAndreas Gohr * Handle inline editing 234731b875SAndreas Gohr */ 244731b875SAndreas Gohrclass action_plugin_struct_inline extends DokuWiki_Action_Plugin { 254731b875SAndreas Gohr 2694c9aa4cSAndreas Gohr /** @var AccessTableData */ 274731b875SAndreas Gohr protected $schemadata = null; 284731b875SAndreas Gohr 294731b875SAndreas Gohr /** @var Column */ 304731b875SAndreas Gohr protected $column = null; 314731b875SAndreas Gohr 324731b875SAndreas Gohr /** @var String */ 334731b875SAndreas Gohr protected $pid = ''; 344731b875SAndreas Gohr 354731b875SAndreas Gohr /** 364731b875SAndreas Gohr * Registers a callback function for a given event 374731b875SAndreas Gohr * 384731b875SAndreas Gohr * @param Doku_Event_Handler $controller DokuWiki's event controller object 394731b875SAndreas Gohr * @return void 404731b875SAndreas Gohr */ 414731b875SAndreas Gohr public function register(Doku_Event_Handler $controller) { 424731b875SAndreas Gohr $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax'); 434731b875SAndreas Gohr } 444731b875SAndreas Gohr 454731b875SAndreas Gohr /** 464731b875SAndreas Gohr * @param Doku_Event $event 474731b875SAndreas Gohr * @param $param 484731b875SAndreas Gohr */ 494731b875SAndreas Gohr public function handle_ajax(Doku_Event $event, $param) { 504731b875SAndreas Gohr $len = strlen('plugin_struct_inline_'); 514731b875SAndreas Gohr if(substr($event->data, 0, $len) != 'plugin_struct_inline_') return; 524731b875SAndreas Gohr $event->preventDefault(); 534731b875SAndreas Gohr $event->stopPropagation(); 544731b875SAndreas Gohr 554731b875SAndreas Gohr if(substr($event->data, $len) == 'editor') { 564731b875SAndreas Gohr $this->inline_editor(); 574731b875SAndreas Gohr } 584731b875SAndreas Gohr 594731b875SAndreas Gohr if(substr($event->data, $len) == 'save') { 604731b875SAndreas Gohr try { 614731b875SAndreas Gohr $this->inline_save(); 624731b875SAndreas Gohr } catch(StructException $e) { 634731b875SAndreas Gohr http_status(500); 644731b875SAndreas Gohr header('Content-Type: text/plain; charset=utf-8'); 654731b875SAndreas Gohr echo $e->getMessage(); 664731b875SAndreas Gohr } 674731b875SAndreas Gohr } 68cdd09a96SAndreas Gohr 69cdd09a96SAndreas Gohr if(substr($event->data, $len) == 'cancel') { 70cdd09a96SAndreas Gohr $this->inline_cancel(); 71cdd09a96SAndreas Gohr } 724731b875SAndreas Gohr } 734731b875SAndreas Gohr 74cdd09a96SAndreas Gohr /** 75cdd09a96SAndreas Gohr * Creates the inline editor 76cdd09a96SAndreas Gohr */ 774731b875SAndreas Gohr protected function inline_editor() { 78cdd09a96SAndreas Gohr // silently fail when editing not possible 794731b875SAndreas Gohr if(!$this->initFromInput()) return; 80cdd09a96SAndreas Gohr if(auth_quickaclcheck($this->pid) < AUTH_EDIT) return; 816ebbbb8eSAndreas Gohr if(!$this->schemadata->getSchema()->isEditable()) return; 82cdd09a96SAndreas Gohr if(checklock($this->pid)) return; 834731b875SAndreas Gohr 84cdd09a96SAndreas Gohr // lock page 85cdd09a96SAndreas Gohr lock($this->pid); 864731b875SAndreas Gohr 87cdd09a96SAndreas Gohr // output the editor 884731b875SAndreas Gohr $value = $this->schemadata->getDataColumn($this->column); 89ee983135SMichael Große $id = uniqid('struct__', false); 907c4f397eSRandolf Rotta echo '<div class="field">'; 91ee983135SMichael Große echo '<label data-column="' . hsc($this->column->getFullQualifiedLabel()) . '" for="' . $id . '">'; 9263e019d0SAndreas Gohr echo '</label>'; 937c4f397eSRandolf Rotta echo '<span class="input">'; 94ee983135SMichael Große echo $value->getValueEditor('entry', $id); 957c4f397eSRandolf Rotta echo '</span>'; 964731b875SAndreas Gohr $hint = $this->column->getType()->getTranslatedHint(); 974731b875SAndreas Gohr if($hint) { 98712bc832SMichael Große echo '<p class="hint">'; 994731b875SAndreas Gohr echo hsc($hint); 100712bc832SMichael Große echo '</p>'; 1014731b875SAndreas Gohr } 1027c4f397eSRandolf Rotta echo '</div>'; 103cdd09a96SAndreas Gohr 104cdd09a96SAndreas Gohr // csrf protection 105cdd09a96SAndreas Gohr formSecurityToken(); 1064731b875SAndreas Gohr } 1074731b875SAndreas Gohr 108cdd09a96SAndreas Gohr /** 109cdd09a96SAndreas Gohr * Save the data posted by the inline editor 110cdd09a96SAndreas Gohr */ 1114731b875SAndreas Gohr protected function inline_save() { 1124731b875SAndreas Gohr global $INPUT; 1134731b875SAndreas Gohr 11413eddb0fSAndreas Gohr // check preconditions 1154d2da382SAndreas Gohr if(!$this->initFromInput()) { 1164d2da382SAndreas Gohr throw new StructException('inline save error: init'); 1174d2da382SAndreas Gohr } 11813eddb0fSAndreas Gohr self::checkCSRF(); 119*0ceefd5cSAnna Dabrowska if(!$this->schemadata->getRid()) { 12013eddb0fSAndreas Gohr $this->checkPage(); 1214eed39ffSMichael Grosse $assignments = Assignments::getInstance(); 1224eed39ffSMichael Grosse $tables = $assignments->getPageAssignments($this->pid, true); 1234eed39ffSMichael Grosse if (!in_array($this->schemadata->getSchema()->getTable(), $tables)) { 1244eed39ffSMichael Grosse throw new StructException('inline save error: schema not assigned to page'); 1254eed39ffSMichael Grosse } 1264731b875SAndreas Gohr } 1276ebbbb8eSAndreas Gohr if(!$this->schemadata->getSchema()->isEditable()) { 1286ebbbb8eSAndreas Gohr throw new StructException('inline save error: no permission for schema'); 1296ebbbb8eSAndreas Gohr } 1304731b875SAndreas Gohr 1314731b875SAndreas Gohr // validate 1324731b875SAndreas Gohr $value = $INPUT->param('entry'); 13393ca6f4fSAndreas Gohr $validator = new ValueValidator(); 1344731b875SAndreas Gohr if(!$validator->validateValue($this->column, $value)) { 1354731b875SAndreas Gohr throw new StructException(join("\n", $validator->getErrors())); 1364731b875SAndreas Gohr } 1374731b875SAndreas Gohr 1384731b875SAndreas Gohr // current data 1394731b875SAndreas Gohr $tosave = $this->schemadata->getDataArray(); 1404731b875SAndreas Gohr $tosave[$this->column->getLabel()] = $value; 1414731b875SAndreas Gohr 1424731b875SAndreas Gohr // save 143*0ceefd5cSAnna Dabrowska if($this->schemadata->getRid() && !$this->schemadata->getPid()) { 14413eddb0fSAndreas Gohr $revision = 0; 14513eddb0fSAndreas Gohr } else { 14613eddb0fSAndreas Gohr $revision = helper_plugin_struct::createPageRevision($this->pid, 'inline edit'); 147858c5caaSMichael Grosse p_get_metadata($this->pid); // reparse the metadata of the page top update the titles/rev/lasteditor table 14813eddb0fSAndreas Gohr } 14913eddb0fSAndreas Gohr $this->schemadata->setTimestamp($revision); 15013eddb0fSAndreas Gohr try { 15113eddb0fSAndreas Gohr if(!$this->schemadata->saveData($tosave)) { 15213eddb0fSAndreas Gohr throw new StructException('saving failed'); 15313eddb0fSAndreas Gohr } 154*0ceefd5cSAnna Dabrowska if(!$this->schemadata->getRid()) { 1554eed39ffSMichael Grosse // make sure this schema is assigned 156b8cff1dfSAndreas Gohr /** @noinspection PhpUndefinedVariableInspection */ 1574eed39ffSMichael Grosse $assignments->assignPageSchema( 1584eed39ffSMichael Grosse $this->pid, 1594eed39ffSMichael Grosse $this->schemadata->getSchema()->getTable() 1604eed39ffSMichael Grosse ); 1614eed39ffSMichael Grosse } 162b8cff1dfSAndreas Gohr } catch (\Exception $e) { 163b8cff1dfSAndreas Gohr // PHP <7 needs a catch block 164b8cff1dfSAndreas Gohr throw $e; 16513eddb0fSAndreas Gohr } finally { 16613eddb0fSAndreas Gohr // unlock (unlocking a non-existing file is okay, 16713eddb0fSAndreas Gohr // so we don't check if it's a lookup here 168cdd09a96SAndreas Gohr unlock($this->pid); 16913eddb0fSAndreas Gohr } 1704731b875SAndreas Gohr 1714731b875SAndreas Gohr // reinit then render 1724731b875SAndreas Gohr $this->initFromInput(); 1734731b875SAndreas Gohr $value = $this->schemadata->getDataColumn($this->column); 1744731b875SAndreas Gohr $R = new Doku_Renderer_xhtml(); 1754731b875SAndreas Gohr $value->render($R, 'xhtml'); // FIXME use configured default renderer 1764731b875SAndreas Gohr echo $R->doc; 1774731b875SAndreas Gohr } 1784731b875SAndreas Gohr 1794731b875SAndreas Gohr /** 180cdd09a96SAndreas Gohr * Unlock a page (on cancel action) 181cdd09a96SAndreas Gohr */ 182cdd09a96SAndreas Gohr protected function inline_cancel() { 183cdd09a96SAndreas Gohr global $INPUT; 184cdd09a96SAndreas Gohr $pid = $INPUT->str('pid'); 185cdd09a96SAndreas Gohr unlock($pid); 186cdd09a96SAndreas Gohr } 187cdd09a96SAndreas Gohr 188cdd09a96SAndreas Gohr /** 1894731b875SAndreas Gohr * Initialize internal state based on input variables 1904731b875SAndreas Gohr * 1914731b875SAndreas Gohr * @return bool if initialization was successfull 1924731b875SAndreas Gohr */ 1934731b875SAndreas Gohr protected function initFromInput() { 1944731b875SAndreas Gohr global $INPUT; 1954731b875SAndreas Gohr 1964731b875SAndreas Gohr $this->schemadata = null; 1974731b875SAndreas Gohr $this->column = null; 1984731b875SAndreas Gohr 1994731b875SAndreas Gohr $pid = $INPUT->str('pid'); 2004731b875SAndreas Gohr list($table, $field) = explode('.', $INPUT->str('field')); 2014731b875SAndreas Gohr if(blank($pid)) return false; 2024731b875SAndreas Gohr if(blank($table)) return false; 2034731b875SAndreas Gohr if(blank($field)) return false; 2044731b875SAndreas Gohr 2054731b875SAndreas Gohr $this->pid = $pid; 2064ec54c67SAndreas Gohr try { 2074ec54c67SAndreas Gohr $this->schemadata = AccessTable::byTableName($table, $pid); 2084ec54c67SAndreas Gohr } catch(StructException $ignore) { 2094731b875SAndreas Gohr return false; 2104731b875SAndreas Gohr } 2114731b875SAndreas Gohr 2124ec54c67SAndreas Gohr $this->column = $this->schemadata->getSchema()->findColumn($field); 2134731b875SAndreas Gohr if(!$this->column || !$this->column->isVisibleInEditor()) { 2144731b875SAndreas Gohr $this->schemadata = null; 2154731b875SAndreas Gohr $this->column = null; 2164731b875SAndreas Gohr return false; 2174731b875SAndreas Gohr } 2184731b875SAndreas Gohr 2194731b875SAndreas Gohr return true; 2204731b875SAndreas Gohr } 2214731b875SAndreas Gohr 22213eddb0fSAndreas Gohr /** 22313eddb0fSAndreas Gohr * Checks if a page can be edited 22413eddb0fSAndreas Gohr * 22513eddb0fSAndreas Gohr * @throws StructException when check fails 22613eddb0fSAndreas Gohr */ 22713eddb0fSAndreas Gohr protected function checkPage() { 22813eddb0fSAndreas Gohr if(!page_exists($this->pid)) { 22913eddb0fSAndreas Gohr throw new StructException('inline save error: no such page'); 23013eddb0fSAndreas Gohr } 23113eddb0fSAndreas Gohr if(auth_quickaclcheck($this->pid) < AUTH_EDIT) { 23213eddb0fSAndreas Gohr throw new StructException('inline save error: acl'); 23313eddb0fSAndreas Gohr } 23413eddb0fSAndreas Gohr if(checklock($this->pid)) { 23513eddb0fSAndreas Gohr throw new StructException('inline save error: lock'); 23613eddb0fSAndreas Gohr } 23713eddb0fSAndreas Gohr } 23813eddb0fSAndreas Gohr 23913eddb0fSAndreas Gohr /** 24013eddb0fSAndreas Gohr * Our own implementation of checkSecurityToken because we don't want the msg() call 24113eddb0fSAndreas Gohr * 24213eddb0fSAndreas Gohr * @throws StructException when check fails 24313eddb0fSAndreas Gohr */ 244c498205aSAndreas Gohr public static function checkCSRF() { 24513eddb0fSAndreas Gohr global $INPUT; 24613eddb0fSAndreas Gohr if( 24713eddb0fSAndreas Gohr $INPUT->server->str('REMOTE_USER') && 24813eddb0fSAndreas Gohr getSecurityToken() != $INPUT->str('sectok') 24913eddb0fSAndreas Gohr ) { 25013eddb0fSAndreas Gohr throw new StructException('CSRF check failed'); 25113eddb0fSAndreas Gohr } 25213eddb0fSAndreas Gohr } 25313eddb0fSAndreas Gohr 2544731b875SAndreas Gohr} 255