EGroupware /
egroupware
| 1 | <?php |
||||
| 2 | /** |
||||
| 3 | * EGroupware - eTemplate serverside implementation of the nextmatch widget |
||||
| 4 | * |
||||
| 5 | * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License |
||||
| 6 | * @package api |
||||
| 7 | * @subpackage etemplate |
||||
| 8 | * @link http://www.egroupware.org |
||||
| 9 | * @author Ralf Becker <[email protected]> |
||||
| 10 | * @copyright 2002-19 by [email protected] |
||||
| 11 | */ |
||||
| 12 | |||||
| 13 | namespace EGroupware\Api\Etemplate\Widget; |
||||
| 14 | |||||
| 15 | use EGroupware\Api\Etemplate; |
||||
| 16 | use EGroupware\Api; |
||||
| 17 | |||||
| 18 | /** |
||||
| 19 | * eTemplate serverside implementation of the nextmatch widget |
||||
| 20 | * |
||||
| 21 | * $content[$id] = array( // I = value set by the app, 0 = value on return / output |
||||
| 22 | * 'get_rows' => // I method/callback to request the data for the rows eg. 'notes.bo.get_rows' |
||||
| 23 | * 'cat_id_label' => // I label for category (optional) |
||||
| 24 | * 'filter_label' => // I label for filter (optional) |
||||
| 25 | * 'filter2_label' => // I label for filter2 (optional) |
||||
| 26 | * 'filter_help' => // I help-msg for filter (optional) |
||||
| 27 | * 'no_filter' => True// I disable the 1. filter |
||||
| 28 | * 'no_filter2' => True// I disable the 2. filter (params are the same as for filter) |
||||
| 29 | * 'no_cat' => True// I disable the cat-selectbox |
||||
| 30 | * 'cat_app' => // I application the cat's should be from, default app in get_rows |
||||
| 31 | * 'cat_is_select' => // I true||'no_lang' use selectbox instead of category selection, default null |
||||
| 32 | * 'template' => // I template to use for the rows, if not set via options |
||||
| 33 | * 'header_left' => // I template to show left of the range-value, left-aligned (optional) |
||||
| 34 | * 'header_right' => // I template to show right of the range-value, right-aligned (optional) |
||||
| 35 | * 'bottom_too' => True// I show the nextmatch-line (arrows, filters, search, ...) again after the rows |
||||
| 36 | * 'never_hide' => True// I never hide the nextmatch-line if less then maxmatch entries |
||||
| 37 | * 'lettersearch' => True// I show a lettersearch |
||||
| 38 | * 'searchletter' => // IO active letter of the lettersearch or false for [all] |
||||
| 39 | * 'start' => // IO position in list |
||||
| 40 | * 'num_rows' => // IO number of rows to show, defaults to maxmatches from the general prefs |
||||
| 41 | * 'cat_id' => // IO category, if not 'no_cat' => True |
||||
| 42 | * 'search' => // IO search pattern |
||||
| 43 | * 'order' => // IO name of the column to sort after (optional for the sortheaders) |
||||
| 44 | * 'sort' => // IO direction of the sort: 'ASC' or 'DESC' |
||||
| 45 | * 'col_filter' => // IO array of column-name value pairs (optional for the filterheaders) |
||||
| 46 | * // grid requires implementation of folowing filters in get_rows, even if not used as regular filters! |
||||
| 47 | * // O col_filter[$row_id] to query certain rows only |
||||
| 48 | * // O col_filter[$parent_id] row_id of parent to query children for hierachical display |
||||
| 49 | * 'filter' => // IO filter, if not 'no_filter' => True |
||||
| 50 | * 'filter_no_lang' => True// I set no_lang for filter (=dont translate the options) |
||||
| 51 | * 'filter_onchange'=> 'this.form.submit();' // I onChange action for filter, default: this.form.submit(); |
||||
| 52 | * 'filter2' => // IO filter2, if not 'no_filter2' => True |
||||
| 53 | * 'filter2_no_lang'=> True// I set no_lang for filter2 (=dont translate the options) |
||||
| 54 | * 'filter2_onchange'=> 'this.form.submit();' // I onChange action for filter2, default: this.form.submit(); |
||||
| 55 | * 'rows' => // O content set by callback |
||||
| 56 | * 'total' => // O the total number of entries |
||||
| 57 | * 'sel_options' => // O additional or changed sel_options set by the callback and merged into $tmpl->sel_options |
||||
| 58 | * 'no_columnselection' => // I turns off the columnselection completly, turned on by default |
||||
| 59 | * 'columnselection_pref' => // I name of the preference (plus 'nextmatch-' prefix), default = template-name |
||||
| 60 | * 'default_cols' => // I columns to use if there's no user or default pref (! as first char uses all but the named columns), default all columns |
||||
| 61 | * 'options-selectcols' => // I array with name/label pairs for the column-selection, this gets autodetected by default. A name => false suppresses a column completly. |
||||
| 62 | * 'return' => // IO allows to return something from the get_rows function if $query is a var-param! |
||||
| 63 | * 'csv_fields' => // I false=disable csv export, true or unset=enable it with auto-detected fieldnames or preferred importexport definition, |
||||
| 64 | * array with name=>label or name=>array('label'=>label,'type'=>type) pairs (type is a eT widget-type) |
||||
| 65 | * or name of import/export definition |
||||
| 66 | * 'row_id' => // I key into row content to set it's value as row-id, eg. 'id' |
||||
| 67 | * 'row_modified' => // I key into row content for modification date or state of a row, to not query it again |
||||
| 68 | * 'parent_id' => // I key into row content of children linking them to their parent, also used as col_filter to query children |
||||
| 69 | * 'is_parent' => // I key into row content to mark a row to have children |
||||
| 70 | * 'is_parent_value'=> // I if set value of is_parent, otherwise is_parent is evaluated as boolean |
||||
| 71 | * 'dataStorePrefix' => // I Optional prefix for client side cache to prevent collisions in applications that have more than one data set, such as ProjectManager / Project elements. Defaults to appname if not set. |
||||
| 72 | * 'actions' => // I array with actions, see nextmatch_widget::egw_actions |
||||
| 73 | * 'action_links' => // I array with enabled actions or ones which should be checked if they are enabled |
||||
| 74 | * optional, default id of all first level actions plus the ones with enabled='javaScript:...' |
||||
| 75 | * 'action_var' => 'action' // I name of var to return choosen action, default 'action' |
||||
| 76 | * 'action' => // O string selected action |
||||
| 77 | * 'selected' => // O array with selected id's |
||||
| 78 | * 'checkboxes' => // O array with checkbox id as key and boolean checked value |
||||
| 79 | * 'select_all' => // O boolean value of select_all checkbox, reference to above value for key 'select_all' |
||||
| 80 | * 'favorites' => // I boolean|array True to enable favorites, or an array of additional, app specific settings to include |
||||
| 81 | * in the saved filters (eg: pm_id) |
||||
| 82 | * 'placeholder' => // I String Optional text to display in the empty row placeholder. If not provided, it's "No matches found." |
||||
| 83 | * 'placeholder_actions' => // I Array Optional list of actions allowed on the placeholder. If not provided, it's ["add"]. |
||||
| 84 | */ |
||||
| 85 | class Nextmatch extends Etemplate\Widget |
||||
| 86 | { |
||||
| 87 | /** |
||||
| 88 | * Path where the icons are stored (relative to webserver_url) |
||||
| 89 | */ |
||||
| 90 | const ICON_PATH = '/api/images'; |
||||
| 91 | |||||
| 92 | public function __construct($xml='') |
||||
| 93 | { |
||||
| 94 | if($xml) { |
||||
| 95 | parent::__construct($xml); |
||||
| 96 | } |
||||
| 97 | } |
||||
| 98 | |||||
| 99 | /** |
||||
| 100 | * Legacy options |
||||
| 101 | */ |
||||
| 102 | protected $legacy_options = 'template'; |
||||
| 103 | |||||
| 104 | /** |
||||
| 105 | * Number of rows to send initially |
||||
| 106 | */ |
||||
| 107 | const INITIAL_ROWS = 50; |
||||
| 108 | |||||
| 109 | /** |
||||
| 110 | * Set up what we know on the server side. |
||||
| 111 | * |
||||
| 112 | * Sending a first chunk of rows |
||||
| 113 | * |
||||
| 114 | * @param string $cname |
||||
| 115 | * @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont' |
||||
| 116 | */ |
||||
| 117 | public function beforeSendToClient($cname, array $expand=null) |
||||
| 118 | { |
||||
| 119 | $form_name = self::form_name($cname, $this->id, $expand); |
||||
| 120 | $value = self::get_array(self::$request->content, $form_name, true); |
||||
| 121 | |||||
| 122 | $value['start'] = 0; |
||||
| 123 | if(!array_key_exists('num_rows',$value)) |
||||
| 124 | { |
||||
| 125 | $value['num_rows'] = self::INITIAL_ROWS; |
||||
| 126 | } |
||||
| 127 | |||||
| 128 | $value['rows'] = array(); |
||||
| 129 | |||||
| 130 | $send_value = $value; |
||||
| 131 | |||||
| 132 | list($app) = explode('.',$value['get_rows']); |
||||
| 133 | if(!$GLOBALS['egw_info']['apps'][$app]) |
||||
| 134 | { |
||||
| 135 | list($app) = explode('.',$this->attrs['template']); |
||||
| 136 | } |
||||
| 137 | |||||
| 138 | // Check for a favorite in URL |
||||
| 139 | if($_GET['favorite'] && $value['favorites']) |
||||
| 140 | { |
||||
| 141 | $safe_name = preg_replace('/[^A-Za-z0-9-_]/','_',strip_tags($_GET['favorite'])); |
||||
| 142 | $pref_name = "favorite_" .$safe_name; |
||||
| 143 | |||||
| 144 | // Do some easy applying of filters server side |
||||
| 145 | $favorite = $GLOBALS['egw_info']['user']['preferences'][$app][$pref_name]; |
||||
| 146 | if(!$favorite && $_GET['favorite'] == 'blank') |
||||
| 147 | { |
||||
| 148 | // Have to go through each of these |
||||
| 149 | foreach(array('search','cat_id','filter','filter2') as $filter) |
||||
| 150 | { |
||||
| 151 | $send_value[$filter] = ''; |
||||
| 152 | } |
||||
| 153 | unset($send_value['col_filter']); |
||||
| 154 | } |
||||
| 155 | // Old type |
||||
| 156 | if($favorite && $favorite['filter']) |
||||
| 157 | { |
||||
| 158 | $favorite['state'] = $favorite['filter']; |
||||
| 159 | } |
||||
| 160 | if($favorite && $favorite['state']) |
||||
| 161 | { |
||||
| 162 | $send_value = array_merge($value, $favorite['state']); |
||||
| 163 | |||||
| 164 | // Ajax call can handle the saved sort here, but this can't |
||||
| 165 | if($favorite['state']['sort']) |
||||
| 166 | { |
||||
| 167 | unset($send_value['sort']); |
||||
| 168 | $send_value['order'] = $favorite['state']['sort']['id']; |
||||
| 169 | $send_value['sort'] = $favorite['state']['sort']['asc'] ? 'ASC' : 'DESC'; |
||||
| 170 | } |
||||
| 171 | } |
||||
| 172 | } |
||||
| 173 | // Make sure it's not set |
||||
| 174 | unset($send_value['favorite']); |
||||
| 175 | |||||
| 176 | // Parse sort into something that get_rows functions are expecting: db_field in order, ASC/DESC in sort |
||||
| 177 | if(is_array($send_value['sort'])) |
||||
| 178 | { |
||||
| 179 | $send_value['order'] = $send_value['sort']['id']; |
||||
| 180 | $send_value['sort'] = $send_value['sort']['asc'] ? 'ASC' : 'DESC'; |
||||
| 181 | } |
||||
| 182 | if($value['num_rows'] != 0) |
||||
| 183 | { |
||||
| 184 | $total = self::call_get_rows($send_value, $send_value['rows'], self::$request->readonlys, null, null, $this); |
||||
| 185 | } |
||||
| 186 | if (true) $value =& self::get_array(self::$request->content, $form_name, true); |
||||
| 187 | |||||
| 188 | // Add favorite here so app doesn't save it in the session |
||||
| 189 | if($_GET['favorite']) |
||||
| 190 | { |
||||
| 191 | $send_value['favorite'] = $safe_name; |
||||
| 192 | } |
||||
| 193 | if (true) $value = $send_value; |
||||
| 194 | $value['total'] = $total; |
||||
| 195 | |||||
| 196 | // Send categories |
||||
| 197 | if(!$value['no_cat'] && !$value['cat_is_select']) |
||||
| 198 | { |
||||
| 199 | $cat_app = $value['cat_app'] ? $value['cat_app'] : $GLOBALS['egw_info']['flags']['current_app']; |
||||
| 200 | $value['options-cat_id'] = self::$request->sel_options['cat_id'] ? self::$request->sel_options['cat_id'] : array(); |
||||
| 201 | |||||
| 202 | // Add 'All', if not already there |
||||
| 203 | if(!$value['options-cat_id'][''] && !$value['options-cat_id'][0]) |
||||
| 204 | { |
||||
| 205 | $value['options-cat_id'][''] = lang('All categories'); |
||||
| 206 | } |
||||
| 207 | $value['options-cat_id'] += Select::typeOptions('select-cat', ',,'.$cat_app,$no_lang=true,false,$value['cat_id']); |
||||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
| 208 | Select::fix_encoded_options($value['options-cat_id']); |
||||
| 209 | } |
||||
| 210 | |||||
| 211 | // Favorite group for admins |
||||
| 212 | if($GLOBALS['egw_info']['apps']['admin'] && $value['favorites']) |
||||
| 213 | { |
||||
| 214 | self::$request->sel_options[$form_name]['favorite']['group'] = array('all' => lang('All users')) + |
||||
| 215 | Select::typeOptions('select-account',',groups'); |
||||
| 216 | } |
||||
| 217 | foreach($value as $name => &$_value) |
||||
| 218 | { |
||||
| 219 | if(strpos($name, 'options-') !== false && $_value) |
||||
| 220 | { |
||||
| 221 | $select = substr($name, 8); |
||||
| 222 | if(!self::$request->sel_options[$select]) |
||||
| 223 | { |
||||
| 224 | self::$request->sel_options[$select] = array(); |
||||
| 225 | } |
||||
| 226 | Select::fix_encoded_options($_value, TRUE); |
||||
| 227 | self::$request->sel_options[$select] += $_value; |
||||
| 228 | // The client doesn't need them in content, but we can't unset them because |
||||
| 229 | // some apps don't send them on re-load, pulling them from the session |
||||
| 230 | //unset($value[$name]); |
||||
| 231 | } |
||||
| 232 | } |
||||
| 233 | if($value['rows']['sel_options']) |
||||
| 234 | { |
||||
| 235 | self::$request->sel_options = array_merge(self::$request->sel_options,$value['rows']['sel_options']); |
||||
| 236 | unset($value['rows']['sel_options']); |
||||
| 237 | } |
||||
| 238 | |||||
| 239 | // If column selection preference is forced, set a flag to turn off UI |
||||
| 240 | $pref_name = 'nextmatch-' . (isset($value['columnselection_pref']) ? $value['columnselection_pref'] : $this->attrs['template']); |
||||
| 241 | $value['no_columnselection'] = $value['no_columnselection'] || ( |
||||
| 242 | $GLOBALS['egw']->preferences->forced[$app][$pref_name] && |
||||
| 243 | // Need to check admin too, or it will be impossible to turn off |
||||
| 244 | !$GLOBALS['egw_info']['user']['apps']['admin'] |
||||
| 245 | ); |
||||
| 246 | // Use this flag to indicate to the admin that columns are forced (and that's why they can't change) |
||||
| 247 | $value['columns_forced'] = (boolean)$GLOBALS['egw']->preferences->forced[$app][$pref_name]; |
||||
| 248 | |||||
| 249 | // todo: no need to store rows in request, it's enought to send them to client |
||||
| 250 | |||||
| 251 | //error_log(__METHOD__."() $this: total=$value[total]"); |
||||
| 252 | //foreach($value['rows'] as $n => $row) error_log("$n: ".array2string($row)); |
||||
| 253 | |||||
| 254 | // set up actions, but only if they are defined AND not already set up (run throught self::egw_actions()) |
||||
| 255 | if (isset($value['actions']) && !isset($value['actions'][0])) |
||||
| 256 | { |
||||
| 257 | $value['action_links'] = array(); |
||||
| 258 | $template_name = isset($value['template']) ? $value['template'] : $this->attrs['options']; |
||||
| 259 | if (!is_array($value['action_links'])) $value['action_links'] = array(); |
||||
| 260 | $value['actions'] = self::egw_actions($value['actions'], $template_name, '', $value['action_links']); |
||||
| 261 | } |
||||
| 262 | } |
||||
| 263 | |||||
| 264 | /** |
||||
| 265 | * Callback to fetch more rows |
||||
| 266 | * |
||||
| 267 | * Callback uses existing get_rows callback, but requires now 'row_id' to be set. |
||||
| 268 | * If no 'row_modified' is set, rows cant checked for modification and therefore |
||||
| 269 | * are always returned to client if in range or deleted if outside range. |
||||
| 270 | * |
||||
| 271 | * @param string $exec_id identifys the etemplate request |
||||
| 272 | * @param array $queriedRange array with values for keys "start", "num_rows" and optional "refresh", "parent_id" |
||||
| 273 | * @param array $filters Search and filter parameters, passed to data source |
||||
| 274 | * @param string $form_name ='nm' full id of widget incl. all namespaces |
||||
| 275 | * @param array $knownUids =null uid's know to client |
||||
| 276 | * @param int $lastModified =null date $knowUids last checked |
||||
| 277 | * @todo for $queriedRange[refresh] first check if there's any modification since $lastModified, return $result[order]===null |
||||
| 278 | * @return array with values for keys 'total', 'rows', 'readonlys', 'order', 'data' and 'lastModification' |
||||
| 279 | */ |
||||
| 280 | static public function ajax_get_rows($exec_id, array $queriedRange, array $filters = array(), $form_name='nm', |
||||
| 281 | array $knownUids=null, $lastModified=null) |
||||
| 282 | { |
||||
| 283 | self::$request = Etemplate\Request::read($exec_id); |
||||
|
0 ignored issues
–
show
It seems like
EGroupware\Api\Etemplate\Request::read($exec_id) can also be of type EGroupware\Api\Etemplate\Request. However, the property $request is declared as type EGroupware\Api\Etemplate\etemplate_request. Maybe add an additional type check?
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly. For example, imagine you have a variable Either this assignment is in error or a type check should be added for that assignment. class Id
{
public $id;
public function __construct($id)
{
$this->id = $id;
}
}
class Account
{
/** @var Id $id */
public $id;
}
$account_id = false;
if (starsAreRight()) {
$account_id = new Id(42);
}
$account = new Account();
if ($account instanceof Id)
{
$account->id = $account_id;
}
Loading history...
|
|||||
| 284 | // fix for somehow empty etemplate request content |
||||
| 285 | if (!is_array(self::$request->content)) |
||||
| 286 | { |
||||
| 287 | self::$request->content = array($form_name => array()); |
||||
| 288 | } |
||||
| 289 | self::$response = Api\Json\Response::get(); |
||||
| 290 | |||||
| 291 | $value = self::get_array(self::$request->content, $form_name, true); |
||||
| 292 | if(!is_array($value)) |
||||
| 293 | { |
||||
| 294 | $value = ($value) ? array($value) : array(); |
||||
| 295 | } |
||||
| 296 | |||||
| 297 | // Validate filters |
||||
| 298 | if (($template = Template::instance(self::$request->template['name'], self::$request->template['template_set'], |
||||
| 299 | self::$request->template['version'], self::$request->template['load_via']))) |
||||
| 300 | { |
||||
| 301 | $template = $template->getElementById($form_name, strpos($form_name, 'history') === 0 ? 'historylog' : 'nextmatch'); |
||||
| 302 | $expand = array( |
||||
| 303 | 'cont' => array($form_name => $filters), |
||||
| 304 | ); |
||||
| 305 | $valid_filters = array(); |
||||
| 306 | |||||
| 307 | if($template) |
||||
| 308 | { |
||||
| 309 | $template->run('validate', array('', $expand, $expand['cont'], &$valid_filters), false); // $respect_disabled=false: as client may disable things, here we validate everything and leave it to the get_rows to interpret |
||||
| 310 | $filters = $valid_filters[$form_name]; |
||||
| 311 | } |
||||
| 312 | // Avoid empty arrays, they cause problems with db filtering |
||||
| 313 | foreach((array)$filters['col_filter'] as $col => $val) |
||||
| 314 | { |
||||
| 315 | if(is_array($val) && count($val) == 0) |
||||
| 316 | { |
||||
| 317 | unset($filters['col_filter'][$col]); |
||||
| 318 | } |
||||
| 319 | } |
||||
| 320 | //error_log($this . " Valid filters: " . array2string($filters)); |
||||
| 321 | } |
||||
| 322 | else |
||||
| 323 | { |
||||
| 324 | $template = null; // get_rows method requires null, not false |
||||
| 325 | } |
||||
| 326 | |||||
| 327 | if (true) $value = $value_in = array_merge($value, $filters); |
||||
| 328 | |||||
| 329 | //error_log(__METHOD__."('".substr($exec_id,0,10)."...', range=".array2string($queriedRange).', filters='.array2string($filters).", '$form_name', knownUids=".array2string($knownUids).", lastModified=$lastModified) parent_id=$value[parent_id], is_parent=$value[is_parent]"); |
||||
| 330 | |||||
| 331 | $result = array(); |
||||
| 332 | |||||
| 333 | // Parse sort into something that get_rows functions are expecting: db_field in order, ASC/DESC in sort |
||||
| 334 | if(is_array($value['sort'])) |
||||
| 335 | { |
||||
| 336 | $value['order'] = $value['sort']['id']; |
||||
| 337 | $value['sort'] = $value['sort']['asc'] ? 'ASC' : 'DESC'; |
||||
| 338 | } |
||||
| 339 | |||||
| 340 | $value['start'] = (int)$queriedRange['start']; |
||||
| 341 | $value['num_rows'] = (int)$queriedRange['num_rows']; |
||||
| 342 | if($value['num_rows'] == 0) $value['num_rows'] = self::INITIAL_ROWS; |
||||
| 343 | // if app supports parent_id / hierarchy ($value['parent_id'] not empty), set parent_id as filter |
||||
| 344 | if (($parent_id = $value['parent_id'])) |
||||
| 345 | { |
||||
| 346 | // Infolog at least wants 'parent_id' instead of $parent_id |
||||
| 347 | $value['col_filter'][$parent_id] = $queriedRange['parent_id']; |
||||
| 348 | if ($queriedRange['parent_id']) $value['csv_export'] = 'children'; |
||||
| 349 | } |
||||
| 350 | |||||
| 351 | // Set current app for get_rows |
||||
| 352 | list($app) = explode('.',self::$request->method); |
||||
| 353 | if(!$app) list($app) = explode('::',self::$request->method); |
||||
| 354 | if($app) |
||||
| 355 | { |
||||
| 356 | $GLOBALS['egw_info']['flags']['currentapp'] = $app; |
||||
| 357 | Api\Translation::add_app($app); |
||||
| 358 | } |
||||
| 359 | // If specific data requested, just do that |
||||
| 360 | if (($row_id = $value['row_id']) && $queriedRange['refresh']) |
||||
| 361 | { |
||||
| 362 | $value['col_filter'][$row_id] = $queriedRange['refresh']; |
||||
| 363 | $value['csv_export'] = 'refresh'; |
||||
| 364 | } |
||||
| 365 | $rows = $result['data'] = $result['order'] = array(); |
||||
| 366 | $result['total'] = self::call_get_rows($value, $rows, $result['readonlys'], null, null, $template); |
||||
| 367 | $result['lastModification'] = Api\DateTime::to('now', 'ts')-1; |
||||
| 368 | |||||
| 369 | if (isset($GLOBALS['egw_info']['flags']['app_header']) && self::$request->app_header != $GLOBALS['egw_info']['flags']['app_header']) |
||||
| 370 | { |
||||
| 371 | self::$request->app_header = $GLOBALS['egw_info']['flags']['app_header']; |
||||
| 372 | Api\Json\Response::get()->apply('egw_app_header', array($GLOBALS['egw_info']['flags']['app_header'])); |
||||
| 373 | } |
||||
| 374 | |||||
| 375 | $GLOBALS['egw']->session->commit_session(); |
||||
| 376 | |||||
| 377 | $row_id = isset($value['row_id']) ? $value['row_id'] : 'id'; |
||||
| 378 | $row_modified = $value['row_modified']; |
||||
| 379 | |||||
| 380 | foreach($rows as $n => $row) |
||||
| 381 | { |
||||
| 382 | $kUkey = false; |
||||
| 383 | if (is_int($n) && $row) |
||||
| 384 | { |
||||
| 385 | if (!isset($row[$row_id])) unset($row_id); // unset default row_id of 'id', if not used |
||||
| 386 | if (!isset($row[$row_modified])) unset($row_modified); |
||||
| 387 | |||||
| 388 | $id = $row_id ? $row[$row_id] : $n; |
||||
| 389 | $result['order'][] = $id; |
||||
| 390 | |||||
| 391 | $modified = $row[$row_modified]; |
||||
| 392 | if (isset($modified) && !(is_int($modified) || is_string($modified) && is_numeric($modified))) |
||||
| 393 | { |
||||
| 394 | $modified = Api\DateTime::to(str_replace('Z', '', $modified), 'ts'); |
||||
| 395 | } |
||||
| 396 | |||||
| 397 | // check if we need to send the data |
||||
| 398 | //error_log("$id Known: " . (array_search($id, $knownUids) !== false ? 'Yes' : 'No') . ' Modified: ' . Api\DateTime::to($row[$row_modified]) . ' > ' . Api\DateTime::to($lastModified).'? ' . ($row[$row_modified] > $lastModified ? 'Yes' : 'No')); |
||||
| 399 | if (!$row_id || !$knownUids || ($kUkey = array_search($id, $knownUids)) === false || |
||||
| 400 | !$lastModified || !isset($modified) || $modified > $lastModified || |
||||
| 401 | $queriedRange['refresh'] && $id == $queriedRange['refresh'] |
||||
| 402 | ) |
||||
| 403 | { |
||||
| 404 | $result['data'][$id] = $row; |
||||
| 405 | } |
||||
| 406 | |||||
| 407 | if ($kUkey !== false) unset($knownUids[$kUkey]); |
||||
| 408 | } |
||||
| 409 | else // non-row data set by get_rows method |
||||
| 410 | { |
||||
| 411 | // Encode all select options and re-index to avoid Firefox's problem with |
||||
| 412 | // '' => 'All' |
||||
| 413 | if($n == 'sel_options') |
||||
| 414 | { |
||||
| 415 | foreach($row as &$options) |
||||
| 416 | { |
||||
| 417 | Select::fix_encoded_options($options,true); |
||||
| 418 | } |
||||
| 419 | } |
||||
| 420 | $result['rows'][$n] = $row; |
||||
| 421 | } |
||||
| 422 | } |
||||
| 423 | // check knowUids outside of range for modification - includes deleted |
||||
| 424 | /* |
||||
| 425 | if ($knownUids) |
||||
| 426 | { |
||||
| 427 | // row_id not set for nextmatch --> just skip them, we can't identify the rows |
||||
| 428 | if (!$row_id) |
||||
| 429 | { |
||||
| 430 | foreach($knownUids as $uid) |
||||
| 431 | { |
||||
| 432 | // Just don't send it back for now |
||||
| 433 | unset($result['data'][$uid]); |
||||
| 434 | //$result['data'][$uid] = null; |
||||
| 435 | } |
||||
| 436 | } |
||||
| 437 | else |
||||
| 438 | { |
||||
| 439 | error_log(__METHOD__."() knowUids left to check ".array2string($knownUids)); |
||||
| 440 | // check if they are up to date: we create a query similar to csv-export without any filters |
||||
| 441 | $uid_query = $value; |
||||
| 442 | $uid_query['csv_export'] = 'knownUids'; // do not store $value in session |
||||
| 443 | $uid_query['filter'] = $uid_query['filter2'] = $uid_query['cat_id'] = $uid_query['search'] = ''; |
||||
| 444 | $uid_query['col_filter'] = array($row_id => $knownUids); |
||||
| 445 | // if we know name of modification column and have a last-modified date |
||||
| 446 | if ($row_modified && $lastModified) // --> set filter to return only modified entries |
||||
| 447 | { |
||||
| 448 | $uid_query['col_filter'][] = $row_modified.' > '.(int)$lastModified; |
||||
| 449 | } |
||||
| 450 | $uid_query['start'] = 0; |
||||
| 451 | $uid_query['num_rows'] = count($knownUids); |
||||
| 452 | $rows = array(); |
||||
| 453 | try |
||||
| 454 | { |
||||
| 455 | if (self::call_get_rows($uid_query, $rows)) |
||||
| 456 | { |
||||
| 457 | foreach($rows as $n => $row) |
||||
| 458 | { |
||||
| 459 | if (!is_int($n)) continue; // ignore non-row data set by get_rows method |
||||
| 460 | |||||
| 461 | if (!$row_modified || !isset($row[$row_modified]) || |
||||
| 462 | !isset($lastModified) || $row[$row_modified] > $lastModified) |
||||
| 463 | { |
||||
| 464 | $result['data'][$row[$row_id]] = $row; |
||||
| 465 | $kUkey = array_search($id, $knownUids); |
||||
| 466 | if ($kUkey !== false) unset($knownUids[$kUkey]); |
||||
| 467 | } |
||||
| 468 | } |
||||
| 469 | } |
||||
| 470 | } |
||||
| 471 | catch (Exception $e) |
||||
| 472 | { |
||||
| 473 | unset($value['row_modified']); |
||||
| 474 | error_log("Error trying to find changed rows with {$value['get_rows']}, falling back to all rows. "); |
||||
| 475 | error_log($e); |
||||
| 476 | } |
||||
| 477 | |||||
| 478 | // Remove any remaining knownUIDs from the grid |
||||
| 479 | foreach($knownUids as $uid) |
||||
| 480 | { |
||||
| 481 | $result['data'][$uid] = null; |
||||
| 482 | } |
||||
| 483 | } |
||||
| 484 | } |
||||
| 485 | */ |
||||
| 486 | |||||
| 487 | // Check for anything changed in the query |
||||
| 488 | // Tell the client about the changes |
||||
| 489 | $request_value =& self::get_array(self::$request->content, $form_name,true); |
||||
| 490 | $changes = $no_rows = false; |
||||
|
0 ignored issues
–
show
|
|||||
| 491 | |||||
| 492 | foreach(array_keys($value_in) + array_keys($value) as $key) |
||||
| 493 | { |
||||
| 494 | // These keys are ignored |
||||
| 495 | if(in_array($key, array('col_filter','start','num_rows','total','order','sort'))) |
||||
| 496 | { |
||||
| 497 | continue; |
||||
| 498 | } |
||||
| 499 | if($value_in[$key] == $value[$key]) continue; |
||||
| 500 | |||||
| 501 | // These keys we don't send row data back, as they cause a partial reload |
||||
| 502 | if(in_array($key, array('template'))) $no_rows = true; |
||||
| 503 | |||||
| 504 | // Actions still need extra handling |
||||
| 505 | if($key == 'actions' && !isset($value['actions'][0])) |
||||
| 506 | { |
||||
| 507 | $value['action_links'] = array(); |
||||
| 508 | $template_name = isset($value['template']) ? $value['template'] : ''; |
||||
| 509 | if (!is_array($value['action_links'])) $value['action_links'] = array(); |
||||
| 510 | $value['actions'] = self::egw_actions($value['actions'], $template_name, '', $value['action_links']); |
||||
| 511 | } |
||||
| 512 | |||||
| 513 | $changes = true; |
||||
| 514 | $request_value[$key] = $value[$key]; |
||||
| 515 | |||||
| 516 | Api\Json\Response::get()->generic('assign', array( |
||||
| 517 | 'etemplate_exec_id' => $exec_id, |
||||
| 518 | 'id' => $form_name, |
||||
| 519 | 'key' => $key, |
||||
| 520 | 'value' => $value[$key], |
||||
| 521 | )); |
||||
| 522 | } |
||||
| 523 | // Request doesn't handle changing by reference, so force it |
||||
| 524 | if($changes) |
||||
| 525 | { |
||||
| 526 | $content = self::$request->content; |
||||
| 527 | self::$request->content = array(); |
||||
| 528 | self::$request->content = $content; |
||||
| 529 | } |
||||
| 530 | |||||
| 531 | // Send back data |
||||
| 532 | //foreach($result as $name => $value) if ($name != 'readonlys') error_log(__METHOD__."() result['$name']=".array2string($name == 'data' ? array_keys($value) : $value)); |
||||
| 533 | Api\Json\Response::get()->data($result); |
||||
| 534 | |||||
| 535 | // If etemplate_exec_id has changed, update the client side |
||||
| 536 | if (($new_id = self::$request->id()) != $exec_id) |
||||
| 537 | { |
||||
| 538 | Api\Json\Response::get()->generic('assign', array( |
||||
| 539 | 'etemplate_exec_id' => $exec_id, |
||||
| 540 | 'id' => '', |
||||
| 541 | 'key' => 'etemplate_exec_id', |
||||
| 542 | 'value' => $new_id, |
||||
| 543 | )); |
||||
| 544 | } |
||||
| 545 | } |
||||
| 546 | |||||
| 547 | /** |
||||
| 548 | * Calling our callback |
||||
| 549 | * |
||||
| 550 | * Signature of get_rows callback is either: |
||||
| 551 | * a) int get_rows($query,&$rows,&$readonlys) |
||||
| 552 | * b) int get_rows(&$query,&$rows,&$readonlys) |
||||
| 553 | * |
||||
| 554 | * If get_rows is called static (and php >= 5.2.3), it is always b) independent on how it's defined! |
||||
| 555 | * |
||||
| 556 | * @param array &$value |
||||
| 557 | * @param array &$rows on return: rows are indexed by their row-number: $value[start], ..., $value[start]+$value[num_rows]-1 |
||||
| 558 | * @param array &$readonlys =null |
||||
| 559 | * @param object $obj =null (internal) |
||||
| 560 | * @param string|array $method =null (internal) |
||||
| 561 | * @param Etemplate\Widget $widget =null instanciated nextmatch widget to let it's widgets transform each row |
||||
| 562 | * @return int|boolean total items found of false on error ($value['get_rows'] not callable) |
||||
| 563 | */ |
||||
| 564 | private static function call_get_rows(array &$value,array &$rows,array &$readonlys=null,$obj=null,$method=null, Etemplate\Widget $widget=null) |
||||
| 565 | { |
||||
| 566 | if (is_null($method)) $method = $value['get_rows']; |
||||
| 567 | |||||
| 568 | if (is_null($obj)) |
||||
| 569 | { |
||||
| 570 | // allow static callbacks |
||||
| 571 | if(strpos($method,'::') !== false) |
||||
| 572 | { |
||||
| 573 | list($class,$method) = explode('::',$method); |
||||
| 574 | |||||
| 575 | // workaround for php < 5.2.3: do NOT call it static, but allow application code to specify static callbacks |
||||
| 576 | if (version_compare(PHP_VERSION,'5.2.3','>=')) |
||||
| 577 | { |
||||
| 578 | $method = array($class,$method); |
||||
| 579 | unset($class); |
||||
| 580 | } |
||||
| 581 | } |
||||
| 582 | else |
||||
| 583 | { |
||||
| 584 | list($app,$class,$method) = explode('.',$value['get_rows']); |
||||
| 585 | } |
||||
| 586 | if ($class) |
||||
| 587 | { |
||||
| 588 | if (!$app && !is_object($GLOBALS[$class])) |
||||
| 589 | { |
||||
| 590 | $GLOBALS[$class] = new $class(); |
||||
| 591 | } |
||||
| 592 | if (is_object($GLOBALS[$class])) // use existing instance (put there by a previous CreateObject) |
||||
| 593 | { |
||||
| 594 | $obj = $GLOBALS[$class]; |
||||
| 595 | } |
||||
| 596 | else |
||||
| 597 | { |
||||
| 598 | $obj = CreateObject($app.'.'.$class); |
||||
|
0 ignored issues
–
show
The function
CreateObject() has been deprecated: use autoloadable class-names and new
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This function has been deprecated. The supplier of the function has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead. Loading history...
|
|||||
| 599 | } |
||||
| 600 | } |
||||
| 601 | } |
||||
| 602 | $raw_rows = array(); |
||||
| 603 | if (!is_array($readonlys)) $readonlys = array(); |
||||
| 604 | if(is_callable($method)) // php5.2.3+ static call (value is always a var param!) |
||||
| 605 | { |
||||
| 606 | $total = call_user_func_array($method,array(&$value,&$raw_rows,&$readonlys)); |
||||
| 607 | } |
||||
| 608 | elseif(is_object($obj) && method_exists($obj,$method)) |
||||
| 609 | { |
||||
| 610 | $total = $obj->$method($value,$raw_rows,$readonlys); |
||||
| 611 | } |
||||
| 612 | else |
||||
| 613 | { |
||||
| 614 | $total = false; // method not callable |
||||
| 615 | } |
||||
| 616 | |||||
| 617 | // allow to hook into get_rows of other apps |
||||
| 618 | Api\Hooks::process(array( |
||||
| 619 | 'hook_location' => 'etemplate2_after_get_rows', |
||||
| 620 | 'get_rows' => $method, |
||||
| 621 | 'value' => &$value, |
||||
| 622 | 'rows' => &$rows, |
||||
| 623 | 'readonlys' => &$readonlys, |
||||
| 624 | 'total' => &$total, |
||||
| 625 | ), array(), true); // true = no permission check |
||||
| 626 | |||||
| 627 | // if we have a nextmatch widget, find the repeating row |
||||
| 628 | if ($widget && $widget->attrs['template']) |
||||
| 629 | { |
||||
| 630 | $row_template = $widget->getElementById($widget->attrs['template']); |
||||
| 631 | if(!$row_template) |
||||
| 632 | { |
||||
| 633 | $row_template = Template::instance($widget->attrs['template']); |
||||
| 634 | } |
||||
| 635 | |||||
| 636 | // Try to find just the repeating part |
||||
| 637 | $repeating_row = null; |
||||
| 638 | // First child should be a grid, we want last row |
||||
| 639 | foreach($row_template->children[0]->children[1]->children as $child) |
||||
| 640 | { |
||||
| 641 | if($child->type == 'row') $repeating_row = $child; |
||||
| 642 | } |
||||
| 643 | } |
||||
| 644 | // otherwise we might get stoped by max_excutiontime |
||||
| 645 | if ($total > 200) @set_time_limit(0); |
||||
|
0 ignored issues
–
show
It seems like you do not handle an error condition for
set_time_limit(). This can introduce security issues, and is generally not recommended.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
If you suppress an error, we recommend checking for the error condition explicitly: // For example instead of
@mkdir($dir);
// Better use
if (@mkdir($dir) === false) {
throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
|
|||||
| 646 | |||||
| 647 | $is_parent = $value['is_parent']; |
||||
| 648 | $is_parent_value = $value['is_parent_value']; |
||||
| 649 | $parent_id = $value['parent_id']; |
||||
| 650 | |||||
| 651 | // remove empty rows required by old etemplate to compensate for header rows |
||||
| 652 | $first = $total ? null : 0; |
||||
| 653 | foreach($raw_rows as $n => $row) |
||||
| 654 | { |
||||
| 655 | // skip empty rows inserted for each header-line in old etemplate |
||||
| 656 | if (is_int($n) && is_array($rows)) |
||||
| 657 | { |
||||
| 658 | if (is_null($first)) $first = $n; |
||||
| 659 | |||||
| 660 | if ($row[$is_parent]) // if app supports parent_id / hierarchy, set parent_id and is_parent |
||||
| 661 | { |
||||
| 662 | $row['is_parent'] = isset($is_parent_value) ? |
||||
| 663 | $row[$is_parent] == $is_parent_value : (boolean)$row[$is_parent]; |
||||
| 664 | $row['parent_id'] = $row[$parent_id]; // seems NOT used on client! |
||||
| 665 | } |
||||
| 666 | // run beforeSendToClient methods of widgets in row on row-data |
||||
| 667 | if($repeating_row) |
||||
| 668 | { |
||||
| 669 | // Change anything by widget for each row ($row set to 1) |
||||
| 670 | $_row = array(1 => &$row); |
||||
| 671 | $repeating_row->run('set_row_value', array('',array('row' => 1), &$_row), true); |
||||
| 672 | } |
||||
| 673 | else if (!$widget || get_class($widget) != __NAMESPACE__.'\\HistoryLog') |
||||
| 674 | { |
||||
| 675 | // Fallback based on widget names |
||||
| 676 | //error_log(self::$request->template['name'] . ' had to fallback to run_beforeSendToClient() because it could not find the row'); |
||||
| 677 | $row = self::run_beforeSendToClient($row); |
||||
| 678 | } |
||||
| 679 | $rows[$n-$first+$value['start']] = $row; |
||||
| 680 | } |
||||
| 681 | elseif(!is_numeric($n)) // rows with string-keys, after numeric rows |
||||
| 682 | { |
||||
| 683 | if($n == 'sel_options') |
||||
| 684 | { |
||||
| 685 | foreach($row as $name => &$options) |
||||
| 686 | { |
||||
| 687 | // remember newly set options for validation of nextmatch filters |
||||
| 688 | self::$request->sel_options[$name] = $options; |
||||
| 689 | |||||
| 690 | Select::fix_encoded_options($options, true); |
||||
| 691 | } |
||||
| 692 | } |
||||
| 693 | $rows[$n] = $row; |
||||
| 694 | } |
||||
| 695 | } |
||||
| 696 | |||||
| 697 | //error_log($value['get_rows'].'() returning '.array2string($total).', method = '.array2string($method).', value = '.array2string($value)); |
||||
| 698 | return $total; |
||||
| 699 | } |
||||
| 700 | |||||
| 701 | /** |
||||
| 702 | * Run beforeSendToClient methods of widgets in row over row-data |
||||
| 703 | * |
||||
| 704 | * This is currently only a hack to convert everything looking like a timestamp to a 'Y-m-d\TH:i:s\Z' string, fix timezone problems! |
||||
| 705 | * |
||||
| 706 | * @todo instanciate row of template and run it's beforeSendToClient |
||||
| 707 | * @param array $row |
||||
| 708 | * @return array |
||||
| 709 | */ |
||||
| 710 | private static function run_beforeSendToClient(array $row) |
||||
| 711 | { |
||||
| 712 | $timestamps = self::get_timestamps(); |
||||
| 713 | |||||
| 714 | foreach($row as $name => &$value) |
||||
| 715 | { |
||||
| 716 | if ($name[0] != '#' && in_array($name, $timestamps) && $value && |
||||
| 717 | (is_int($value) || is_string($value) && is_numeric($value)) && |
||||
| 718 | ($value > 21000000 || $value < 19000000)) |
||||
| 719 | { |
||||
| 720 | $value = Api\DateTime::to($value, 'Y-m-d\TH:i:s\Z'); |
||||
| 721 | } |
||||
| 722 | } |
||||
| 723 | return $row; |
||||
| 724 | } |
||||
| 725 | |||||
| 726 | /** |
||||
| 727 | * Get all timestamp columns incl. names with removed prefixes like cal_ or contact_ |
||||
| 728 | * |
||||
| 729 | * @return array |
||||
| 730 | */ |
||||
| 731 | private static function get_timestamps() |
||||
| 732 | { |
||||
| 733 | return Api\Cache::getTree(__CLASS__, 'timestamps', function() |
||||
| 734 | { |
||||
| 735 | $timestamps = array(); |
||||
| 736 | foreach(scandir(EGW_SERVER_ROOT) as $app) |
||||
| 737 | { |
||||
| 738 | $dir = EGW_SERVER_ROOT.'/'.$app; |
||||
| 739 | if (is_dir($dir) && file_exists($dir.'/setup/tables_current.inc.php') && |
||||
| 740 | ($tables_defs = $GLOBALS['egw']->db->get_table_definitions($app))) |
||||
| 741 | { |
||||
| 742 | foreach($tables_defs as $defintion) |
||||
| 743 | { |
||||
| 744 | foreach($defintion['fd'] as $col => $data) |
||||
| 745 | { |
||||
| 746 | if ($data['type'] == 'timestamp' || $data['meta'] == 'timestamp') |
||||
| 747 | { |
||||
| 748 | $timestamps[] = $col; |
||||
| 749 | // some apps remove a prefix --> add prefix-less version too |
||||
| 750 | $matches = null; |
||||
| 751 | if (preg_match('/^(tz|acl|async|cal|contact|lock|history|link|cf|cat|et)_(.+)$/', $col, $matches)) |
||||
| 752 | { |
||||
| 753 | $timestamps[] = $matches[2]; |
||||
| 754 | } |
||||
| 755 | } |
||||
| 756 | } |
||||
| 757 | } |
||||
| 758 | } |
||||
| 759 | } |
||||
| 760 | //error_log(__METHOD__."() returning ".array2string($timestamps)); |
||||
| 761 | return $timestamps; |
||||
| 762 | }, array(), 86400); // cache for 1 day |
||||
| 763 | } |
||||
| 764 | |||||
| 765 | /** |
||||
| 766 | * Merges actions together with positions based on group parameter |
||||
| 767 | * |
||||
| 768 | * @param array $actions |
||||
| 769 | * @param array $actions2 |
||||
| 770 | * @return array |
||||
| 771 | */ |
||||
| 772 | static function merge_actions_by_group(array $actions, array $actions2) |
||||
| 773 | { |
||||
| 774 | //error_log(__METHOD__.'('.array2string($actions).', '.array2string($actions2).')'); |
||||
| 775 | |||||
| 776 | //return array_merge_recursive($actions, $actions2); |
||||
| 777 | foreach($actions2 as $name => $action) |
||||
| 778 | { |
||||
| 779 | // overwrite existing action of given name or append action without group |
||||
| 780 | if (isset($actions[$name]) || !isset($action['group'])) |
||||
| 781 | { |
||||
| 782 | $actions[$name] = $action; |
||||
| 783 | } |
||||
| 784 | // find position to insert action |
||||
| 785 | else |
||||
| 786 | { |
||||
| 787 | $n = 0; |
||||
| 788 | foreach($actions as $a) |
||||
| 789 | { |
||||
| 790 | if ($a['group'] > $action['group']) break; |
||||
| 791 | ++$n; |
||||
| 792 | } |
||||
| 793 | $actions = array_merge(array_slice($actions, 0, $n), |
||||
| 794 | array($name => $action), |
||||
| 795 | array_slice($actions, $n, count($actions)-$n)); |
||||
| 796 | } |
||||
| 797 | } |
||||
| 798 | //error_log(__METHOD__.'() returning '.array2string($actions)); |
||||
| 799 | return $actions; |
||||
| 800 | } |
||||
| 801 | |||||
| 802 | /** |
||||
| 803 | * Default maximum length for context submenus, longer menus are put as a "More" submenu |
||||
| 804 | */ |
||||
| 805 | const DEFAULT_MAX_MENU_LENGTH = 14; |
||||
| 806 | |||||
| 807 | /** |
||||
| 808 | * Return egw_actions |
||||
| 809 | * |
||||
| 810 | * The following attributes are understood for actions on eTemplate/PHP side: |
||||
| 811 | * - string 'id' id of the action (set as key not attribute!) |
||||
| 812 | * - string 'caption' name/label or action, get's automatic translated |
||||
| 813 | * - boolean 'no_lang' do NOT translate caption, default false |
||||
| 814 | * - string 'icon' icon, eg. 'edit' or 'infolog/task', if no app given app of template or API is used |
||||
| 815 | * - string 'iconUrl' full url of icon, better use 'icon' |
||||
| 816 | * - boolean|string 'allowOnMultiple' should action be shown if multiple lines are marked, or string 'only', default true! |
||||
| 817 | * - boolean|string 'enabled' is action available, or string with javascript function to call, default true! |
||||
| 818 | * - string 'disableClass' class name to check if action should be disabled (if presend, enabled if not) |
||||
| 819 | * (add that css class in get_rows(), if row lacks rights for an action) |
||||
| 820 | * - string 'enableClass' class name to check if action should be enabled (if present, disabled if not) |
||||
| 821 | * - string 'enableId' regular expression row-id has to match to enable action |
||||
| 822 | * - boolean 'hideOnDisabled' hide disabled actions, default false |
||||
| 823 | * - string 'type' type of action, default 'popup' for contenxt menus, 'drag' or 'drop' |
||||
| 824 | * - boolean 'default' is that action the default action, default false |
||||
| 825 | * - array 'children' array with actions of submenu |
||||
| 826 | * - int 'group' to group items, default all actions are in one group |
||||
| 827 | * - string 'onExecute' javascript to run, default 'javaScript:nm_action' or eg. 'javaScript:app.myapp.someMethod' |
||||
| 828 | * which runs action specified in nm_action attribute: |
||||
| 829 | * - string 'nm_action' |
||||
| 830 | * + 'alert' debug action, shows alert with action caption, id and id's of selected rows |
||||
| 831 | * + 'submit' default action, sets nm[action], nm[selected] and nm[select_all] |
||||
| 832 | * + 'location' redirects / set location.href to 'url' attribute |
||||
| 833 | * + 'popup' opens popup with url given in 'url' attribute |
||||
| 834 | * - string 'url' url for location or popup |
||||
| 835 | * - string 'target' target for location or popup |
||||
| 836 | * - string 'width' for popup |
||||
| 837 | * - string 'height' for popup |
||||
| 838 | * - string 'confirm' confirmation message |
||||
| 839 | * - string 'confirm_multiple' confirmation message for multiple selected, defaults to 'confirm' |
||||
| 840 | * - boolean 'postSubmit' eg. downloads need a submit via POST request not our regular Ajax submit, only works with nm_action=submit! |
||||
| 841 | * - string 'hint' tooltip on menu item |
||||
| 842 | * |
||||
| 843 | * @param array $actions id indexed array of actions / array with valus for keys: 'iconUrl', 'caption', 'onExecute', ... |
||||
| 844 | * @param string $template_name ='' name of the template, used as default for app name of images |
||||
| 845 | * @param string $prefix ='' prefix for ids |
||||
| 846 | * @param array &$action_links =array() on return all first-level actions plus the ones with enabled='javaScript:...' |
||||
| 847 | * @param int $max_length =self::DEFAULT_MAX_MENU_LENGTH automatic pagination, not for first menu level! |
||||
| 848 | * @param array $default_attrs =null default attributes |
||||
| 849 | * @return array |
||||
| 850 | */ |
||||
| 851 | public static function egw_actions(array $actions=null, $template_name='', $prefix='', array &$action_links=array(), |
||||
| 852 | $max_length=self::DEFAULT_MAX_MENU_LENGTH, array $default_attrs=null) |
||||
| 853 | { |
||||
| 854 | //echo "<p>".__METHOD__."(\$actions, '$template_name', '$prefix', \$action_links, $max_length) \$actions="; _debug_array($actions); |
||||
| 855 | $first_level = !$action_links; // add all first level actions |
||||
| 856 | |||||
| 857 | if ($first_level) |
||||
| 858 | { |
||||
| 859 | // allow other apps to add actions |
||||
| 860 | foreach(Api\Hooks::process(array( |
||||
| 861 | 'location' => 'add_row_actions', |
||||
| 862 | 'template_name' => $template_name, |
||||
| 863 | ), array('policy'), true) as $app => $data) |
||||
| 864 | { |
||||
| 865 | // todo: place new items based on group |
||||
| 866 | if ($data) $actions = self::merge_actions_by_group((array)$actions, $data); |
||||
| 867 | } |
||||
| 868 | } |
||||
| 869 | |||||
| 870 | //echo "actions="; _debug_array($actions); |
||||
| 871 | $egw_actions = array(); |
||||
| 872 | $n = 1; |
||||
| 873 | $group = false; |
||||
| 874 | |||||
| 875 | foreach((array)$actions as $id => $action) |
||||
| 876 | { |
||||
| 877 | if (!empty($action['hideOnMobile']) && Api\Header\UserAgent::mobile()) |
||||
| 878 | { |
||||
| 879 | continue; // no need to send this action to client, specially document actions can be huge |
||||
| 880 | } |
||||
| 881 | if (!empty($action['disableIfNoEPL']) && $action['disableIfNoEPL'] && !$GLOBALS['egw_info']['apps']['stylite']) |
||||
| 882 | { |
||||
| 883 | $action['enabled'] = |
||||
| 884 | $action['hideOnDisabled'] = false; |
||||
| 885 | $action['hint'] = Lang("This feature is only available in EPL version."); |
||||
| 886 | } |
||||
| 887 | else if(!empty($action['disableIfNoEPL'])) |
||||
| 888 | { |
||||
| 889 | unset($action['disableIfNoEPL']); |
||||
| 890 | } |
||||
| 891 | // in case it's only selectbox id => label pairs |
||||
| 892 | if (!is_array($action)) $action = array('caption' => $action); |
||||
| 893 | if ($default_attrs) $action += $default_attrs; |
||||
| 894 | |||||
| 895 | // Add 'Select All' after first group |
||||
| 896 | if ($first_level && $group !== false && $action['group'] != $group && !$egw_actions[$prefix.'select_all']) |
||||
| 897 | { |
||||
| 898 | |||||
| 899 | $egw_actions[$prefix.'select_all'] = array( |
||||
| 900 | 'caption' => 'Select all', |
||||
| 901 | //'checkbox' => true, |
||||
| 902 | 'hint' => 'Select all entries', |
||||
| 903 | 'enabled' => true, |
||||
| 904 | 'shortcut' => array( |
||||
| 905 | 'keyCode' => 65, // A |
||||
| 906 | 'ctrl' => true, |
||||
| 907 | 'caption' => lang('Ctrl').'+A' |
||||
| 908 | ), |
||||
| 909 | 'group' => $action['group'], |
||||
| 910 | ); |
||||
| 911 | $action_links[] = $prefix.'select_all'; |
||||
| 912 | } |
||||
| 913 | $group = $action['group']; |
||||
| 914 | |||||
| 915 | if (!$first_level && $n == $max_length && count($actions) > $max_length) |
||||
| 916 | { |
||||
| 917 | $id = 'more_'.count($actions); // we need a new unique id |
||||
| 918 | $action = array( |
||||
| 919 | 'caption' => 'More', |
||||
| 920 | 'prefix' => $prefix, |
||||
| 921 | // display rest of actions incl. current one as children |
||||
| 922 | 'children' => array_slice($actions, $max_length-1, count($actions)-$max_length+1, true), |
||||
| 923 | ); |
||||
| 924 | //echo "*** Inserting id=$prefix$id"; _debug_array($action); |
||||
| 925 | // we break at end of foreach loop, as rest of actions is already dealt with |
||||
| 926 | // by putting them as children |
||||
| 927 | |||||
| 928 | // sets the default attributes to every children dataset |
||||
| 929 | if (is_array($action['children'])) |
||||
| 930 | { |
||||
| 931 | foreach ($action['children'] as $key => $children) |
||||
| 932 | { |
||||
| 933 | // checks if children is a valid array and if the "$default_attrs" variable exists |
||||
| 934 | if (is_array($children) && $default_attrs) |
||||
| 935 | { |
||||
| 936 | $action['children'][$key] += $default_attrs; |
||||
| 937 | } |
||||
| 938 | } |
||||
| 939 | } |
||||
| 940 | } |
||||
| 941 | |||||
| 942 | // add all first level popup actions plus ones with enabled = 'javaScript:...' to action_links |
||||
| 943 | if ((!isset($action['type']) || in_array($action['type'],array('popup','drag','drop'))) && // popup is the default |
||||
| 944 | ($first_level || substr($action['enabled'],0,11) == 'javaScript:')) |
||||
| 945 | { |
||||
| 946 | $action_links[] = $prefix.$id; |
||||
| 947 | } |
||||
| 948 | |||||
| 949 | // add sub-menues |
||||
| 950 | if ($action['children']) |
||||
| 951 | { |
||||
| 952 | static $inherit_attrs = array('url','popup','nm_action','onExecute','type','egw_open','allowOnMultiple','confirm','confirm_multiple'); |
||||
| 953 | $inherit_keys = array_flip($inherit_attrs); |
||||
| 954 | $action['children'] = self::egw_actions($action['children'], $template_name, $action['prefix'], $action_links, $max_length, |
||||
| 955 | array_intersect_key($action, $inherit_keys)); |
||||
| 956 | |||||
| 957 | unset($action['prefix']); |
||||
| 958 | |||||
| 959 | // Allow default actions to keep their onExecute |
||||
| 960 | if($action['default']) unset($inherit_keys['onExecute']); |
||||
| 961 | $action = array_diff_key($action, $inherit_keys); |
||||
| 962 | } |
||||
| 963 | |||||
| 964 | // link or popup action |
||||
| 965 | if ($action['url']) |
||||
| 966 | { |
||||
| 967 | $action['url'] = Api\Framework::link('/index.php',str_replace('$action',$id,$action['url'])); |
||||
| 968 | if ($action['popup']) |
||||
| 969 | { |
||||
| 970 | list($action['data']['width'],$action['data']['height']) = explode('x',$action['popup']); |
||||
| 971 | unset($action['popup']); |
||||
| 972 | $action['data']['nm_action'] = 'popup'; |
||||
| 973 | } |
||||
| 974 | else |
||||
| 975 | { |
||||
| 976 | $action['data']['nm_action'] = 'location'; |
||||
| 977 | if(!$action['target'] && strpos($action['url'],'menuaction') > 0) |
||||
| 978 | { |
||||
| 979 | // It would be better if app set target, but we'll auto-detect if not |
||||
| 980 | list(,$menuaction) = explode('=',$action['url']); |
||||
| 981 | list($app) = explode('.',$menuaction); |
||||
| 982 | $action['data']['target'] = $app; |
||||
| 983 | } |
||||
| 984 | } |
||||
| 985 | } |
||||
| 986 | if ($action['egw_open']) |
||||
| 987 | { |
||||
| 988 | $action['data']['nm_action'] = 'egw_open'; |
||||
| 989 | } |
||||
| 990 | |||||
| 991 | $egw_actions[$prefix.$id] = $action; |
||||
| 992 | |||||
| 993 | if (!$first_level && $n++ == $max_length) break; |
||||
| 994 | } |
||||
| 995 | |||||
| 996 | // Make sure select all is in a group by itself |
||||
| 997 | foreach($egw_actions as $id => &$_action) |
||||
| 998 | { |
||||
| 999 | if($id == $prefix . 'select_all') continue; |
||||
| 1000 | if($_action['group'] >= $egw_actions[$prefix.'select_all']['group'] ) |
||||
| 1001 | { |
||||
| 1002 | $egw_actions[$id]['group']+=1; |
||||
| 1003 | } |
||||
| 1004 | } |
||||
| 1005 | //echo "egw_actions="; _debug_array($egw_actions); |
||||
| 1006 | return $egw_actions; |
||||
| 1007 | } |
||||
| 1008 | |||||
| 1009 | /** |
||||
| 1010 | * Action with submenu for categories |
||||
| 1011 | * |
||||
| 1012 | * Automatic switch to hierarchical display, if more then $max_cats_flat=14 cats found. |
||||
| 1013 | * |
||||
| 1014 | * @param string $app |
||||
| 1015 | * @param int $group =0 see self::egw_actions |
||||
| 1016 | * @param string $caption ='Change category' |
||||
| 1017 | * @param string $prefix ='cat_' prefix category id to get action id |
||||
| 1018 | * @param boolean $globals =true application global categories too |
||||
| 1019 | * @param int $parent_id =0 only returns cats of a certain parent |
||||
| 1020 | * @param int $max_cats_flat =self::DEFAULT_MAX_MENU_LENGTH use hierarchical display if more cats |
||||
| 1021 | * @return array like self::egw_actions |
||||
| 1022 | */ |
||||
| 1023 | public static function category_action($app, $group=0, $caption='Change category', |
||||
| 1024 | $prefix='cat_', $globals=true, $parent_id=0, $max_cats_flat=self::DEFAULT_MAX_MENU_LENGTH) |
||||
| 1025 | { |
||||
| 1026 | $cat = new Api\Categories(null,$app); |
||||
| 1027 | $cats = $cat->return_sorted_array($start=0, false, '', 'ASC', 'cat_name', $globals, $parent_id, true); |
||||
| 1028 | |||||
| 1029 | // if more then max_length cats, switch automatically to hierarchical display |
||||
| 1030 | if (count($cats) > $max_cats_flat) |
||||
| 1031 | { |
||||
| 1032 | $cat_actions = self::category_hierarchy($cats, $prefix, $parent_id); |
||||
| 1033 | } |
||||
| 1034 | else // flat, indented categories |
||||
| 1035 | { |
||||
| 1036 | $cat_actions = array(); |
||||
| 1037 | foreach((array)$cats as $cat) |
||||
| 1038 | { |
||||
| 1039 | $name = str_repeat(' ',2*$cat['level']) . stripslashes($cat['name']); |
||||
| 1040 | |||||
| 1041 | $cat_actions[$cat['id']] = array( |
||||
| 1042 | 'caption' => $name, |
||||
| 1043 | 'no_lang' => true, |
||||
| 1044 | ); |
||||
| 1045 | // add category icon |
||||
| 1046 | if (is_array($cat['data']) && $cat['data']['icon'] && file_exists(EGW_SERVER_ROOT.self::ICON_PATH.'/'.basename($cat['data']['icon']))) |
||||
| 1047 | { |
||||
| 1048 | $cat_actions[$cat['id']]['iconUrl'] = $GLOBALS['egw_info']['server']['webserver_url'].self::ICON_PATH.'/'.$cat['data']['icon']; |
||||
| 1049 | } |
||||
| 1050 | } |
||||
| 1051 | } |
||||
| 1052 | return array( |
||||
| 1053 | 'caption' => $caption, |
||||
| 1054 | 'children' => $cat_actions, |
||||
| 1055 | 'enabled' => (boolean)$cat_actions, |
||||
| 1056 | 'group' => $group, |
||||
| 1057 | 'prefix' => $prefix, |
||||
| 1058 | ); |
||||
| 1059 | } |
||||
| 1060 | |||||
| 1061 | /** |
||||
| 1062 | * Return one level of the category hierarchy |
||||
| 1063 | * |
||||
| 1064 | * @param array $cats =null all cats if already read |
||||
| 1065 | * @param string $prefix ='cat_' prefix category id to get action id |
||||
| 1066 | * @param int $parent_id =0 only returns cats of a certain parent |
||||
| 1067 | * @return array |
||||
| 1068 | */ |
||||
| 1069 | private static function category_hierarchy(array $cats, $prefix, $parent_id=0) |
||||
| 1070 | { |
||||
| 1071 | $cat_actions = array(); |
||||
| 1072 | foreach($cats as $key => $cat) |
||||
| 1073 | { |
||||
| 1074 | // current hierarchy level |
||||
| 1075 | if ($cat['parent'] == $parent_id) |
||||
| 1076 | { |
||||
| 1077 | $name = stripslashes($cat['name']); |
||||
| 1078 | |||||
| 1079 | $cat_actions[$cat['id']] = array( |
||||
| 1080 | 'caption' => $name, |
||||
| 1081 | 'no_lang' => true, |
||||
| 1082 | 'prefix' => $prefix, |
||||
| 1083 | ); |
||||
| 1084 | // add category icon |
||||
| 1085 | if ($cat['data']['icon'] && file_exists(EGW_SERVER_ROOT.self::ICON_PATH.'/'.basename($cat['data']['icon']))) |
||||
| 1086 | { |
||||
| 1087 | $cat_actions[$cat['id']]['iconUrl'] = $GLOBALS['egw_info']['server']['webserver_url'].self::ICON_PATH.'/'.$cat['data']['icon']; |
||||
| 1088 | } |
||||
| 1089 | unset($cats[$key]); |
||||
| 1090 | } |
||||
| 1091 | // direct children |
||||
| 1092 | elseif(isset($cat_actions[$cat['parent']])) |
||||
| 1093 | { |
||||
| 1094 | $cat_actions['sub_'.$cat['parent']] = $cat_actions[$cat['parent']]; |
||||
| 1095 | // have to add category itself to children, to be able to select it! |
||||
| 1096 | $cat_actions[$cat['parent']]['group'] = -1; // own group on top |
||||
| 1097 | $cat_actions['sub_'.$cat['parent']]['children'] = array( |
||||
| 1098 | $cat['parent'] => $cat_actions[$cat['parent']], |
||||
| 1099 | )+self::category_hierarchy($cats, $prefix, $cat['parent']); |
||||
| 1100 | unset($cat_actions[$cat['parent']]); |
||||
| 1101 | } |
||||
| 1102 | } |
||||
| 1103 | return $cat_actions; |
||||
| 1104 | } |
||||
| 1105 | |||||
| 1106 | /** |
||||
| 1107 | * Validate input |
||||
| 1108 | * |
||||
| 1109 | * Following attributes get checked: |
||||
| 1110 | * - needed: value must NOT be empty |
||||
| 1111 | * - min, max: int and float widget only |
||||
| 1112 | * - maxlength: maximum length of string (longer strings get truncated to allowed size) |
||||
| 1113 | * - preg: perl regular expression incl. delimiters (set by default for int, float and colorpicker) |
||||
| 1114 | * - int and float get casted to their type |
||||
| 1115 | * |
||||
| 1116 | * @param string $cname current namespace |
||||
| 1117 | * @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont' |
||||
| 1118 | * @param array $content |
||||
| 1119 | * @param array &$validated =array() validated content |
||||
| 1120 | */ |
||||
| 1121 | public function validate($cname, array $expand, array $content, &$validated=array()) |
||||
| 1122 | { |
||||
| 1123 | $form_name = self::form_name($cname, $this->id, $expand); |
||||
| 1124 | $value = self::get_array($content, $form_name); |
||||
| 1125 | |||||
| 1126 | // Some (most) nextmatch settings are set in its value, not attributes, which aren't in |
||||
| 1127 | // $content. Fetch them from the request, so we actually have them. |
||||
| 1128 | $content_value = self::get_array(self::$request->content, $form_name); |
||||
| 1129 | |||||
| 1130 | list($app) = explode('.',$this->attrs['template']); |
||||
| 1131 | |||||
| 1132 | unset($value['favorite']); |
||||
| 1133 | |||||
| 1134 | // On client, rows does not get its own namespace, but all apps are expecting it |
||||
| 1135 | $value['rows'] = $value; |
||||
| 1136 | |||||
| 1137 | // Legacy support - action popups were not properly namespaced |
||||
| 1138 | $preserve = self::get_array(self::$request->preserv, $form_name); |
||||
| 1139 | if($value[$preserve['action_var']] && $content[$value[$preserve['action_var']].'_popup']) |
||||
| 1140 | { |
||||
| 1141 | $validated += $content[$value[$preserve['action_var']].'_popup']; |
||||
| 1142 | } |
||||
| 1143 | |||||
| 1144 | // Save current column settings as default, clear, or force (admins only) |
||||
| 1145 | if($GLOBALS['egw_info']['user']['apps']['admin'] && $app && $value['selectcols']) |
||||
| 1146 | { |
||||
| 1147 | $pref_name = 'nextmatch-' . (isset($content_value['columnselection_pref']) ? $content_value['columnselection_pref'] : $this->attrs['template']); |
||||
| 1148 | $refresh_pref_name = $pref_name.'-autorefresh'; |
||||
| 1149 | switch($value['nm_col_preference']) { |
||||
| 1150 | case 'force': |
||||
| 1151 | $pref_level = 'forced'; |
||||
| 1152 | break; |
||||
| 1153 | case 'reset': |
||||
| 1154 | case 'default': |
||||
| 1155 | $pref_level = 'default'; |
||||
| 1156 | break; |
||||
| 1157 | default: |
||||
| 1158 | $pref_level = 'user'; |
||||
| 1159 | } |
||||
| 1160 | |||||
| 1161 | // Clear forced pref before setting default |
||||
| 1162 | if($pref_level != 'forced') |
||||
| 1163 | { |
||||
| 1164 | $GLOBALS['egw']->preferences->delete($app,$pref_name,'forced'); |
||||
| 1165 | $GLOBALS['egw']->preferences->delete($app,$refresh_pref_name,'forced'); |
||||
| 1166 | $GLOBALS['egw']->preferences->delete($app,$pref_name.'-size','forced'); |
||||
| 1167 | $GLOBALS['egw']->preferences->delete($app,$pref_name.'-lettersearch','forced'); |
||||
| 1168 | $GLOBALS['egw']->preferences->save_repository(true,'forced'); |
||||
| 1169 | } |
||||
| 1170 | |||||
| 1171 | // Set columns + refresh as default for all users |
||||
| 1172 | // Columns included in submit, preference might not be updated yet |
||||
| 1173 | $cols = $value['selectcols']; |
||||
| 1174 | $GLOBALS['egw']->preferences->read_repository(true); |
||||
| 1175 | $GLOBALS['egw']->preferences->add($app,$pref_name,is_array($cols) ? implode(',',$cols) : $cols, $pref_level); |
||||
| 1176 | |||||
| 1177 | // Autorefresh |
||||
| 1178 | $refresh = $value['nm_autorefresh']; |
||||
| 1179 | $GLOBALS['egw']->preferences->add($app,$refresh_pref_name,(int)$refresh,$pref_level); |
||||
| 1180 | |||||
| 1181 | // Lettersearch |
||||
| 1182 | $lettersearch = is_array($cols) && in_array('lettersearch', $cols); |
||||
| 1183 | $GLOBALS['egw']->preferences->add($app,$pref_name.'-lettersearch',(int)$lettersearch,$pref_level); |
||||
| 1184 | |||||
| 1185 | $GLOBALS['egw']->preferences->save_repository(true,$pref_level); |
||||
| 1186 | $GLOBALS['egw']->preferences->read(true); |
||||
| 1187 | |||||
| 1188 | if($value['nm_col_preference'] == 'reset') |
||||
| 1189 | { |
||||
| 1190 | // Clear column + refresh preference so users go back to default |
||||
| 1191 | $GLOBALS['egw']->preferences->delete_preference($app,$pref_name); |
||||
| 1192 | $GLOBALS['egw']->preferences->delete_preference($app,$pref_name.'-size'); |
||||
| 1193 | $GLOBALS['egw']->preferences->delete_preference($app,$pref_name.'-lettersearch'); |
||||
| 1194 | $GLOBALS['egw']->preferences->delete_preference($app,$refresh_pref_name); |
||||
| 1195 | } |
||||
| 1196 | } |
||||
| 1197 | unset($value['nm_col_preference']); |
||||
| 1198 | |||||
| 1199 | $validated[$form_name] = $value; |
||||
| 1200 | } |
||||
| 1201 | |||||
| 1202 | /** |
||||
| 1203 | * Run a given method on all children |
||||
| 1204 | * |
||||
| 1205 | * Reimplemented to add namespace, and make sure row template gets included |
||||
| 1206 | * |
||||
| 1207 | * @param string|callable $method_name or function($cname, $expand, $widget) |
||||
| 1208 | * @param array $params =array('') parameter(s) first parameter has to be cname, second $expand! |
||||
| 1209 | * @param boolean $respect_disabled =false false (default): ignore disabled, true: method is NOT run for disabled widgets AND their children |
||||
| 1210 | */ |
||||
| 1211 | public function run($method_name, $params=array(''), $respect_disabled=false) |
||||
| 1212 | { |
||||
| 1213 | $old_param0 = $params[0]; |
||||
| 1214 | $cname =& $params[0]; |
||||
| 1215 | // Need this check or the headers will get involved too |
||||
| 1216 | if($this->type == 'nextmatch') |
||||
| 1217 | { |
||||
| 1218 | parent::run($method_name, $params, $respect_disabled); |
||||
| 1219 | if ($this->id) $cname = self::form_name($cname, $this->id, $params[1]); |
||||
| 1220 | |||||
| 1221 | // Run on all the sub-templates |
||||
| 1222 | foreach(array('template', 'header_left', 'header_right', 'header_row') as $sub_template) |
||||
| 1223 | { |
||||
| 1224 | if($this->attrs[$sub_template]) |
||||
| 1225 | { |
||||
| 1226 | $row_template = Template::instance($this->attrs[$sub_template]); |
||||
| 1227 | $row_template->run($method_name, $params, $respect_disabled); |
||||
| 1228 | } |
||||
| 1229 | } |
||||
| 1230 | } |
||||
| 1231 | $params[0] = $old_param0; |
||||
| 1232 | |||||
| 1233 | // Prevent troublesome keys from breaking the nextmatch |
||||
| 1234 | // TODO: Figure out where these come from |
||||
| 1235 | foreach(array('$row','${row}', '$', '0','1','2') as $key) |
||||
| 1236 | { |
||||
| 1237 | if(is_array(self::$request->content[$cname])) unset(self::$request->content[$cname][$key]); |
||||
| 1238 | if(is_array(self::$request->preserve[$cname])) unset(self::$request->preserve[$cname][$key]); |
||||
| 1239 | } |
||||
| 1240 | } |
||||
| 1241 | |||||
| 1242 | /** |
||||
| 1243 | * Refresh given rows for specified change |
||||
| 1244 | * |
||||
| 1245 | * Change type parameters allows for quicker refresh then complete server side reload: |
||||
| 1246 | * - edit: send just modified data from given rows |
||||
| 1247 | * - delete: just send null for given rows to clientside (no backend call neccessary) |
||||
| 1248 | * - add: requires full reload |
||||
| 1249 | * |
||||
| 1250 | * @param array|string $row_ids rows to refresh |
||||
| 1251 | * @param string $type ='edit' "edit" (default), "delete" or "add" |
||||
| 1252 | */ |
||||
| 1253 | public function refresh($row_ids, $type='edit') |
||||
| 1254 | { |
||||
| 1255 | unset($row_ids, $type); // not used, but required by function signature |
||||
| 1256 | |||||
| 1257 | throw new Api\Exception('Not yet implemented'); |
||||
| 1258 | } |
||||
| 1259 | } |
||||
| 1260 | |||||
| 1261 | // Registration needs to go here, otherwise customfields won't be loaded until some other cf shows up |
||||
| 1262 | Etemplate\Widget::registerWidget(__NAMESPACE__.'\\Customfields', array('nextmatch-customfields')); |
||||
| 1263 |