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\AccessTableData; 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 25 /** @var AccessTableData */ 26 protected $schemadata = null; 27 28 /** @var Column */ 29 protected $column = null; 30 31 /** @var String */ 32 protected $pid = ''; 33 34 /** @var int */ 35 protected $rid = 0; 36 37 /** 38 * Registers a callback function for a given event 39 * 40 * @param Doku_Event_Handler $controller DokuWiki's event controller object 41 * @return void 42 */ 43 public function register(Doku_Event_Handler $controller) 44 { 45 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjax'); 46 } 47 48 /** 49 * @param Doku_Event $event 50 * @param $param 51 */ 52 public function handleAjax(Doku_Event $event, $param) 53 { 54 $len = strlen('plugin_struct_inline_'); 55 if (substr($event->data, 0, $len) != 'plugin_struct_inline_') return; 56 $event->preventDefault(); 57 $event->stopPropagation(); 58 59 if (substr($event->data, $len) == 'editor') { 60 $this->inlineEditor(); 61 } 62 63 if (substr($event->data, $len) == 'save') { 64 try { 65 $this->inlineSave(); 66 } catch (StructException $e) { 67 http_status(500); 68 header('Content-Type: text/plain; charset=utf-8'); 69 echo $e->getMessage(); 70 } 71 } 72 73 if (substr($event->data, $len) == 'cancel') { 74 $this->inlineCancel(); 75 } 76 } 77 78 /** 79 * Creates the inline editor 80 */ 81 protected function inlineEditor() 82 { 83 // silently fail when editing not possible 84 if (!$this->initFromInput()) return; 85 if (auth_quickaclcheck($this->pid) < AUTH_EDIT) return; 86 if (!$this->schemadata->getSchema()->isEditable()) 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 $this->schemadata = AccessTable::byTableName($table, $pid, $rev, $rid); 221 } catch (StructException $ignore) { 222 return false; 223 } 224 225 $this->column = $this->schemadata->getSchema()->findColumn($field); 226 if (!$this->column || !$this->column->isVisibleInEditor()) { 227 $this->schemadata = null; 228 $this->column = null; 229 return false; 230 } 231 232 return true; 233 } 234 235 /** 236 * Checks if a page can be edited 237 * 238 * @throws StructException when check fails 239 */ 240 protected function checkPage() 241 { 242 if (!page_exists($this->pid)) { 243 throw new StructException('inline save error: no such page'); 244 } 245 if (auth_quickaclcheck($this->pid) < AUTH_EDIT) { 246 throw new StructException('inline save error: acl'); 247 } 248 if (checklock($this->pid)) { 249 throw new StructException('inline save error: lock'); 250 } 251 } 252 253 /** 254 * Our own implementation of checkSecurityToken because we don't want the msg() call 255 * 256 * @throws StructException when check fails 257 */ 258 public static function checkCSRF() 259 { 260 global $INPUT; 261 if ( 262 $INPUT->server->str('REMOTE_USER') && 263 getSecurityToken() != $INPUT->str('sectok') 264 ) { 265 throw new StructException('CSRF check failed'); 266 } 267 } 268} 269