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