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