1<?php 2 3/** 4 * DokuWiki Plugin struct (Action Component) 5 * 6 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 7 * @author Andreas Gohr, Michael Große <dokuwiki@cosmocode.de> 8 */ 9 10use dokuwiki\plugin\struct\meta\AccessTable; 11use dokuwiki\plugin\struct\meta\AccessTablePage; 12use dokuwiki\plugin\struct\meta\Assignments; 13use dokuwiki\plugin\struct\meta\Column; 14use dokuwiki\plugin\struct\meta\StructException; 15use dokuwiki\plugin\struct\meta\ValueValidator; 16 17/** 18 * Class action_plugin_struct_inline 19 * 20 * Handle inline editing 21 */ 22class action_plugin_struct_inline extends DokuWiki_Action_Plugin 23{ 24 /** @var AccessTablePage */ 25 protected $schemadata = null; 26 27 /** @var Column */ 28 protected $column = null; 29 30 /** @var String */ 31 protected $pid = ''; 32 33 /** @var int */ 34 protected $rid = 0; 35 36 /** 37 * Registers a callback function for a given event 38 * 39 * @param Doku_Event_Handler $controller DokuWiki's event controller object 40 * @return void 41 */ 42 public function register(Doku_Event_Handler $controller) 43 { 44 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjax'); 45 } 46 47 /** 48 * @param Doku_Event $event 49 * @param $param 50 */ 51 public function handleAjax(Doku_Event $event, $param) 52 { 53 $len = strlen('plugin_struct_inline_'); 54 if (substr($event->data, 0, $len) != 'plugin_struct_inline_') return; 55 $event->preventDefault(); 56 $event->stopPropagation(); 57 58 if (substr($event->data, $len) == 'editor') { 59 $this->inlineEditor(); 60 } 61 62 if (substr($event->data, $len) == 'save') { 63 try { 64 $this->inlineSave(); 65 } catch (StructException $e) { 66 http_status(500); 67 header('Content-Type: text/plain; charset=utf-8'); 68 echo $e->getMessage(); 69 } 70 } 71 72 if (substr($event->data, $len) == 'cancel') { 73 $this->inlineCancel(); 74 } 75 } 76 77 /** 78 * Creates the inline editor 79 */ 80 protected function inlineEditor() 81 { 82 // silently fail when editing not possible 83 if (!$this->initFromInput()) return; 84 if (!$this->schemadata->getSchema()->isEditable()) return; 85 // only check page permissions for data with pid, skip for global data 86 if ($this->pid && auth_quickaclcheck($this->pid) < AUTH_EDIT) return; 87 if (checklock($this->pid)) return; 88 89 // lock page 90 lock($this->pid); 91 92 // output the editor 93 $value = $this->schemadata->getDataColumn($this->column); 94 $id = uniqid('struct__', false); 95 echo '<div class="field">'; 96 echo '<label data-column="' . hsc($this->column->getFullQualifiedLabel()) . '" for="' . $id . '">'; 97 echo '</label>'; 98 echo '<span class="input">'; 99 echo $value->getValueEditor('entry', $id); 100 echo '</span>'; 101 $hint = $this->column->getType()->getTranslatedHint(); 102 if ($hint) { 103 echo '<p class="hint">'; 104 echo hsc($hint); 105 echo '</p>'; 106 } 107 echo '</div>'; 108 109 // csrf protection 110 formSecurityToken(); 111 } 112 113 /** 114 * Save the data posted by the inline editor 115 */ 116 protected function inlineSave() 117 { 118 global $INPUT; 119 120 // check preconditions 121 if (!$this->initFromInput()) { 122 throw new StructException('inline save error: init'); 123 } 124 self::checkCSRF(); 125 if (!$this->schemadata->getRid()) { 126 $this->checkPage(); 127 $assignments = Assignments::getInstance(); 128 $tables = $assignments->getPageAssignments($this->pid, true); 129 if (!in_array($this->schemadata->getSchema()->getTable(), $tables)) { 130 throw new StructException('inline save error: schema not assigned to page'); 131 } 132 } 133 if (!$this->schemadata->getSchema()->isEditable()) { 134 throw new StructException('inline save error: no permission for schema'); 135 } 136 137 // validate 138 $value = $INPUT->param('entry'); 139 $validator = new ValueValidator(); 140 if (!$validator->validateValue($this->column, $value)) { 141 throw new StructException(join("\n", $validator->getErrors())); 142 } 143 144 // current data 145 $tosave = $this->schemadata->getDataArray(); 146 $tosave[$this->column->getLabel()] = $value; 147 148 // save 149 if ($this->schemadata->getRid()) { 150 $revision = 0; 151 } else { 152 $revision = helper_plugin_struct::createPageRevision($this->pid, 'inline edit'); 153 p_get_metadata($this->pid); // reparse the metadata of the page top update the titles/rev/lasteditor table 154 } 155 $this->schemadata->setTimestamp($revision); 156 try { 157 if (!$this->schemadata->saveData($tosave)) { 158 throw new StructException('saving failed'); 159 } 160 if (!$this->schemadata->getRid()) { 161 // make sure this schema is assigned 162 /** @noinspection PhpUndefinedVariableInspection */ 163 $assignments->assignPageSchema( 164 $this->pid, 165 $this->schemadata->getSchema()->getTable() 166 ); 167 } 168 } catch (\Exception $e) { 169 // PHP <7 needs a catch block 170 throw $e; 171 } finally { 172 // unlock (unlocking a non-existing file is okay, 173 // so we don't check if it's a lookup here 174 unlock($this->pid); 175 } 176 177 // reinit then render 178 $this->initFromInput($this->schemadata->getTimestamp()); 179 $value = $this->schemadata->getDataColumn($this->column); 180 $R = new Doku_Renderer_xhtml(); 181 $value->render($R, 'xhtml'); // FIXME use configured default renderer 182 $data = json_encode(['value' => $R->doc, 'rev' => $this->schemadata->getTimestamp()]); 183 echo $data; 184 } 185 186 /** 187 * Unlock a page (on cancel action) 188 */ 189 protected function inlineCancel() 190 { 191 global $INPUT; 192 $pid = $INPUT->str('pid'); 193 unlock($pid); 194 } 195 196 /** 197 * Initialize internal state based on input variables 198 * 199 * @param int $updatedRev timestamp of currently created revision, might be newer than input variable 200 * @return bool if initialization was successful 201 */ 202 protected function initFromInput($updatedRev = 0) 203 { 204 global $INPUT; 205 206 $this->schemadata = null; 207 $this->column = null; 208 209 $pid = $INPUT->str('pid'); 210 $rid = $INPUT->int('rid'); 211 $rev = $updatedRev ?: $INPUT->int('rev'); 212 213 list($table, $field) = explode('.', $INPUT->str('field')); 214 if (blank($pid) && blank($rid)) return false; 215 if (blank($table)) return false; 216 if (blank($field)) return false; 217 218 $this->pid = $pid; 219 try { 220 if (AccessTable::isTypePage($pid, $rev)) { 221 $this->schemadata = AccessTable::getPageAccess($table, $pid); 222 } elseif (AccessTable::isTypeSerial($pid, $rev)) { 223 $this->schemadata = AccessTable::getSerialAccess($table, $pid, $rid); 224 } else { 225 $this->schemadata = AccessTable::getGlobalAccess($table, $rid); 226 } 227 } catch (StructException $ignore) { 228 return false; 229 } 230 231 $this->column = $this->schemadata->getSchema()->findColumn($field); 232 if (!$this->column || !$this->column->isVisibleInEditor()) { 233 $this->schemadata = null; 234 $this->column = null; 235 return false; 236 } 237 238 return true; 239 } 240 241 /** 242 * Checks if a page can be edited 243 * 244 * @throws StructException when check fails 245 */ 246 protected function checkPage() 247 { 248 if (!page_exists($this->pid)) { 249 throw new StructException('inline save error: no such page'); 250 } 251 if (auth_quickaclcheck($this->pid) < AUTH_EDIT) { 252 throw new StructException('inline save error: acl'); 253 } 254 if (checklock($this->pid)) { 255 throw new StructException('inline save error: lock'); 256 } 257 } 258 259 /** 260 * Our own implementation of checkSecurityToken because we don't want the msg() call 261 * 262 * @throws StructException when check fails 263 */ 264 public static function checkCSRF() 265 { 266 global $INPUT; 267 if ( 268 $INPUT->server->str('REMOTE_USER') && 269 getSecurityToken() != $INPUT->str('sectok') 270 ) { 271 throw new StructException('CSRF check failed'); 272 } 273 } 274} 275