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