PluginProbe
WPJAM Basic / trunk
WPJAM Basic vtrunk
wpjam-basic / includes / class-wpjam-core.php

class-wpjam-core.php in WPJAM Basic trunk, at includes/class-wpjam-core.php

1,509 lines 53.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 class WPJAM_Core extends WPJAM_Register{
3 public function __get($key){
4 if($key == 'title'){
5 return ($labels = $this->builtin('labels')) ? $labels->singular_name : $this->label;
6 }
7
8 $value = $this->builtin($key) ?? parent::__get($key);
9
10 return $key == 'plural' ? ($value ?: ($this->rest_base ?: $this->name.'s')) : $value;
11 }
12
13 public function __set($key, $value){
14 $this->builtin($key, $value);
15
16 return parent::__set($key, $value);
17 }
18
19 public function __call($method, $args){
20 return $this->call_dynamic_method($method, ...$args);
21 }
22
23 public function builtin($key='', ...$args){
24 $type = wpjam_get_builtin_type(get_class($this));
25
26 if(!$key){
27 return $type;
28 }
29
30 if($key != 'name'
31 && property_exists('WP_'.$type, $key)
32 && ($object = wpjam_call('get_'.$type.($type == 'post_type' ? '_object' : ''), $this->name))
33 ){
34 return $args ? ($object->$key = $args[0]) : $object->$key;
35 }
36 }
37
38 public function permastruct(){
39 if(!($value = trim($this->get_arg('permastruct') ?: '', '/'))){
40 return;
41 }
42
43 $name = $this->name;
44 $type = $this->builtin();
45 $hook = 'registered_'.$type.'_'.$name;
46 $qv = $type == 'post_type' ? 'p' : 'term_id';
47 $iv = $qv == 'p' ? '%post_id%' : '%term_id%';
48 $value = str_replace(
49 ($qv == 'p' ? ['%'.$name.'_id%', '%postname%'] : ['%'.$this->query_key.'%', '%termname%']),
50 [$iv, '%'.$name.'%'],
51 $value
52 );
53
54 $this->rewrite = $this->rewrite ?: true;
55
56 if(str_contains($value, $iv)){
57 if($qv == 'p'){
58 if($this->hierarchical){
59 return;
60 }
61 }else{
62 $this->remove_support('slug');
63 }
64
65 $this->query_var ??= false;
66
67 add_action($hook, fn()=> remove_rewrite_tag('%'.$name.'%'));
68
69 add_filter($name.'_rewrite_rules', fn($rules)=> wpjam_map($rules, fn($v)=> str_replace('?'.$qv.'=', '?'.$type.'='.$name.'&'.$qv.'=', $v)));
70 }elseif($value == '%'.$name.'%'){
71 if($qv != 'p'){
72 $this->no_base = true;
73
74 return;
75 }
76 }
77
78 add_action($hook, fn()=> add_permastruct($name, $value, $GLOBALS['wp_rewrite']->extra_permastructs[$name]));
79 }
80
81 public function labels($labels){
82 $l = (array)$labels;
83 $n = $l['name'];
84 $h = $this->hierarchical;
85
86 if($this->builtin() == 'post_type'){
87 $m = ['all_items'=>'所有'.$n, 'archives'=>$n.'归档'];
88 $s = ['撰写新', '写文章', ...($h ? ['页面', 'page', 'Page'] : ['文章', 'post', 'Post'])];
89 $r = ['添加', '添加'.$n, $n, $n, ucfirst($n)];
90 }else{
91 $m = [];
92 $s = $h ? ['分类', 'categories', 'Categories', 'Category'] : ['标签', 'Tag', 'tag'];
93 $r = $h ? [$n, $n.'s', ucfirst($n).'s', ucfirst($n)] : [$n, ucfirst($n), $n];
94 }
95
96 return array_merge(wpjam_map($l, fn($v, $k)=> $m[$k] ?? (($v && $v != $n) ? str_replace($s, $r, $v) : $v)), (array)$this->labels);
97 }
98
99 public function to_array(){
100 $this->filter_args();
101 $this->permastruct();
102
103 if($this->_jam){
104 $type = $this->builtin();
105
106 if(($v = $this->rewrite) !== false){
107 $this->rewrite = (is_array($v) ? $v : [])+['with_front'=>false];
108 }
109
110 if(is_admin() && $this->show_ui){
111 add_filter($type.'_labels_'.$this->name, [$this, 'labels']);
112 }
113
114 wpjam_map($this->options ?:[], fn($v, $k)=> wpjam_register_meta_option($type == 'post_type' ? 'post' : 'term', $k, $v+[$type=>$this->name]));
115 }
116
117 return $this->args;
118 }
119
120 public function add_field($key, ...$args){
121 return $this->process_arg('_fields', fn($v)=> array_merge($v ?: [], is_callable($key) ? [$key] : (is_array($key) ? $key : [$key => $args[0]])));
122 }
123
124 public function remove_field($key){
125 return $this->delete_arg('_fields['.$key.']');
126 }
127
128 public function get_fields($id=0, $action_key=''){
129 return wpjam_reduce($this->get_arg('_fields[]'), fn($c, $v, $k)=> array_merge($c, is_callable($v)
130 ? wpjam_pack(wpjam_try($v, $id, $action_key), is_numeric($k) ? null : $k)
131 : (wpjam_is_assoc_array($v) ? [$k => $v] : [])
132 ), []);
133 }
134
135 public static function filter_builtin_args($args, $name, $object_type=null){
136 if(!static::get($name) && !empty($args['public']) && (did_action('init') || empty($args['_builtin']))){
137 return (static::register($name, ['_jam'=>false]+array_filter(compact('object_type'))+$args))->to_array();
138 }
139
140 return $args;
141 }
142 }
143 /**
144 * @config menu_page, admin_load, register_json, init
145 **/
146 #[config('menu_page', 'admin_load', 'register_json', 'init')]
147 class WPJAM_Post_Type extends WPJAM_Core{
148 public function to_array(){
149 parent::to_array();
150
151 if($this->_jam){
152 $this->public ??= true;
153 $this->supports ??= ['title'];
154
155 if($this->hierarchical){
156 $this->update_arg('supports[]', 'page-attributes');
157 }
158
159 if(!is_array($this->taxonomies)){
160 $this->delete_arg('taxonomies');
161 }
162
163 if($v = $this->menu_icon){
164 $this->menu_icon = wpjam_prefix($v, 'dashicons-');
165 }
166 }
167
168 return $this->args += ['model'=>'WPJAM_Post'];
169 }
170
171 public function get_menu_slug(){
172 return $this->menu_slug ?? (($v = $this->get_arg('menu_page')) && is_array($v) ? (wp_is_numeric_array($v) ? array_first($v) : $v)['menu_slug'] : null);
173 }
174
175 public function get_fields($id=0, $action_key=''){
176 $fields = ($this->supports('images') ? [
177 'images' => ['title'=>'图集', 'type'=>'mu-img', 'name'=>'meta_input[images]', 'item_type'=>'url', 'size'=>($this->images_sizes ?: [''])[0], 'max_items'=>$this->images_max_items]
178 ] : [])+($this->supports('video') ? [
179 'video' => ['title'=>'视频', 'type'=>'url', 'name'=>'meta_input[video]']
180 ] : [])+parent::get_fields($id, $action_key);
181
182 if(in_array($action_key, ['add', 'set'])){
183 $fields = [
184 'post_title' => ['type'=>'text', 'title'=>'标题', 'required']
185 ]+($action_key == 'add' ? [
186 'post_type' => ['type'=>'hidden', 'value'=>$this->name],
187 'post_status' => ['type'=>'hidden', 'value'=>'draft']
188 ] : [])+($this->supports('excerpt') ? [
189 'post_excerpt' => ['title'=>'摘要', 'type'=>'textarea']
190 ] : [])+($this->supports('thumbnail') ? [
191 '_thumbnail_id' => ['title'=>'头图', 'type'=>'img', 'size'=>'600x0', 'name'=>'meta_input[_thumbnail_id]']
192 ] : [])+wpjam_map($fields, fn($v, $k)=> (empty($v['name']) && !property_exists('WP_Post', $k) ? ['name'=>'meta_input['.$k.']'] : [])+$v);
193 }
194
195 return $fields;
196 }
197
198 public function supports($feature){
199 return array_any(wp_parse_list($feature), fn($f)=> post_type_supports($this->name, $f));
200 }
201
202 public function get_taxonomies(...$args){
203 $filter = $args && is_array($args[0]) ? array_shift($args) : [];
204 $output = $args[0] ?? 'objects';
205 $data = get_object_taxonomies($this->name);
206
207 if($filter || $output == 'objects'){
208 $data = wpjam_fill($data, 'wpjam_get_taxonomy');
209 $data = $filter ? wpjam_filter($data, $filter) : $data;
210 $data = $output == 'objects' ? $data : array_keys($data);
211 }
212
213 return $data;
214 }
215
216 public function parse_images($images, $args=[]){
217 $ss = $this->images_sizes;
218 $sizes = [];
219
220 foreach(['large', 'thumbnail'] as $k){
221 if(($v = $args[$k.'_size'] ?? '') !== false){
222 if(!$v && $ss){
223 $v = $ss[$k == 'large' ? 0 : 1];
224
225 if($k == 'thumbnail' && count($images) == 1){
226 $i = array_first($images);
227 $q = wpjam_image($i, 'query') ?: wpjam_tap(wpjam_image($i, 'size') ?: ['width'=>0, 'height'=>0], fn($q)=> update_post_meta($args['post_id'], 'images', [add_query_arg($q, $i)]));
228
229 $v = ($o = $q['orientation'] ?? '') ? ($ss[$o == 'landscape' ? 2 : 3] ?? $v) : $v;
230 }
231 }else{
232 $v = $v ?: $this->{$k.'_size'};
233 }
234
235 $sizes[$k] = $v ?: $k;
236 }
237 }
238
239 $sizes += (!$sizes || ($args['full_size'] ?? true)) ? ['full'=>'full'] : [];
240
241 foreach($images as $image){
242 $parsed = array_map(fn($s)=> wpjam_get_thumbnail($image, $s), $sizes);
243 $parsed = array_merge($parsed, ($query = wpjam_image($image, 'query')) ? wpjam_pick($query, ['orientation', 'width', 'height'])+['width'=>0, 'height'=>0] : []);
244
245 if(isset($sizes['thumbnail'])){
246 $size = wpjam_parse_size($sizes['thumbnail']);
247 $parsed = array_reduce(['width', 'height'], fn($c, $k)=> $c+['thumbnail_'.$k=>$size[$k] ?? 0], $parsed);
248 }
249
250 $results[] = count($sizes) == 1 ? array_first($parsed) : $parsed;
251 }
252
253 return $results;
254 }
255
256 public function reset_invalid_parent($value=0){
257 $wpdb = $GLOBALS['wpdb'];
258 $ids = $wpdb->get_col($wpdb->prepare("SELECT p1.ID FROM {$wpdb->posts} p1 LEFT JOIN {$wpdb->posts} p2 ON p1.post_parent = p2.ID WHERE p1.post_type=%s AND p1.post_parent > 0 AND p2.ID is null", $this->name)) ?: [];
259
260 return count(array_map(fn($id)=> wp_update_post(['ID'=>$id, 'post_parent'=>$value]), $ids));
261 }
262
263 public function registered(){
264 $this->_jam && wpjam_init(fn()=> register_post_type($this->name, $this->to_array()));
265 }
266
267 public static function filter_clauses($clauses, $query){
268 $wpdb = $GLOBALS['wpdb'];
269 $orderby = $query->get('orderby');
270 $order = strtoupper($query->get('order')) == 'ASC' ? 'ASC' : 'DESC';;
271
272 if($orderby == 'related'){
273 if($tt_ids = array_filter(wp_parse_id_list($query->get('term_taxonomy_ids')))){
274 $clauses['join'] .= "INNER JOIN {$wpdb->term_relationships} AS tr ON {$wpdb->posts}.ID = tr.object_id";
275 $clauses['where'] .= " AND tr.term_taxonomy_id IN (".implode(",", $tt_ids).")";
276 $clauses['groupby'] .= " tr.object_id";
277 $clauses['orderby'] = " count(tr.object_id) DESC, {$wpdb->posts}.ID DESC";
278 }
279 }elseif($orderby == 'comment_date'){
280 $ct = $query->get('comment_type') ?: 'comment';
281 $str = $ct == 'comment' ? "'comment', ''" : "'".esc_sql($ct)."'";
282 $where = "ct.comment_type IN ({$str}) AND ct.comment_parent=0 AND ct.comment_approved NOT IN ('spam', 'trash', 'post-trashed')";
283
284 $clauses['join'] = "INNER JOIN {$wpdb->comments} AS ct ON {$wpdb->posts}.ID = ct.comment_post_ID AND {$where}";
285 $clauses['groupby'] = "ct.comment_post_ID";
286 $clauses['orderby'] = "MAX(ct.comment_ID) {$order}";
287 }elseif(in_array($orderby, ['views', 'comment_type'])){
288 $clauses['join'] .= $wpdb->prepare("LEFT JOIN {$wpdb->postmeta} jam_pm ON {$wpdb->posts}.ID = jam_pm.post_id AND jam_pm.meta_key = %s ", sanitize_key($orderby == 'comment_type' ? $query->get('comment_count') : 'views'));
289 $clauses['orderby'] = "(COALESCE(jam_pm.meta_value, 0)+0) {$order}, " . $clauses['orderby'];
290 $clauses['groupby'] = "{$wpdb->posts}.ID";
291 }elseif(in_array($orderby, ['', 'date', 'post_date'])){
292 $clauses['orderby'] .= ", {$wpdb->posts}.ID {$order}";
293 }
294
295 return $clauses;
296 }
297
298 public static function filter_results($posts, $query){
299 $q = &$query->query_vars;
300
301 if(($sticky_posts = array_diff(wp_parse_id_list(wpjam_pull($q, 'sticky_posts') ?: []), $q['post__not_in']))
302 && ($stickies = get_posts([
303 'orderby' => 'post__in',
304 'post__in' => $sticky_posts,
305 'post_type' => $q['post_type'] ?: 'post',
306 'post_status' => 'publish',
307 'posts_per_page' => count($sticky_posts),
308 ]+wpjam_pick($q, ['suppress_filters', 'cache_results', 'update_post_meta_cache', 'update_post_term_cache', 'lazy_load_term_meta'])))
309 ){
310 $q['sticky_posts'] = array_column($stickies, 'ID');
311
312 return array_merge($stickies, array_filter($posts, fn($post)=> !in_array($post->ID, $q['sticky_posts'], true)));
313 }
314
315 return $posts;
316 }
317
318 public static function add_hooks(){
319 $hook = 'content_save_pre';
320 $cb = 'wp_filter_post_kses';
321
322 wpjam_hook($hook, 'tap', fn($c)=> is_serialized($c) && remove_filter($hook, $cb) && wpjam_once($hook, 'tap', fn()=> add_filter($hook, $cb), 11), 1);
323
324 add_filter('post_type_link', fn($link, $post)=> str_replace('%post_id%', $post->ID, $link), 1, 2);
325
326 add_filter('posts_clauses', [static::class, 'filter_clauses'], 1, 2);
327 add_filter('posts_results', [static::class, 'filter_results'], 1, 2);
328 }
329 }
330
331 /**
332 * @config menu_page, admin_load, register_json
333 **/
334 #[config('menu_page', 'admin_load', 'register_json')]
335 class WPJAM_Taxonomy extends WPJAM_Core{
336 public function to_array(){
337 $this->filter_args();
338
339 $this->query_key = ['category'=>'cat', 'post_tag'=>'tag_id'][$this->name] ?? $this->name.'_id';
340 $this->column_name = ['category'=>'categories', 'post_tag'=>'tags'][$this->name] ?? 'taxonomy-'.$this->name;
341 $this->supports = wpjam_array($this->supports ?? ['slug', 'description', ...($this->levels == 1 ? [] : ['parent'])], fn($k, $v)=> is_numeric($k) ? [$v, true] : [$k, $v]);
342
343 parent::to_array();
344
345 return $this->args += ($this->_jam ? ['show_in_nav_menus'=>false, 'show_in_rest'=>true, 'show_admin_column'=>true, 'hierarchical'=>true] : [])+['model'=>'WPJAM_Term', 'show_in_posts_rest'=>$this->show_in_rest];
346 }
347
348 public function add_support($feature, $value=true){
349 return $this->update_arg('supports['.$feature.']', $value);
350 }
351
352 public function remove_support($feature){
353 return $this->delete_arg('supports['.$feature.']');
354 }
355
356 public function supports($feature, ...$args){
357 return is_array($feature) ? array_any($feature, [$this, $supports]) : (bool)$this->get_arg('supports['.$feature.']');
358 }
359
360 public function selectable(){
361 return $this->selectable ?? (wp_count_terms(['taxonomy'=>$this->name, 'hide_empty'=>false]+($this->levels > 1 ? ['parent'=>0] : [])) <= 30);
362 }
363
364 public function get_fields($id=0, $action_key=''){
365 $fields = ($this->supports('thumbnail') ? [
366 'thumbnail' => ['title'=>'缩略图', 'size'=>$this->thumbnail_size]+array_combine(['type', 'item_type'], $this->thumbnail_type == 'image' ? ['image', 'image'] : ['img', 'url'])
367 ] : [])+($this->supports('banner') ? [
368 'banner' => ['title'=>'大图', 'type'=>'img', 'item_type'=>'url', 'size'=>$this->banner_size, 'show_if'=>['parent', -1]]
369 ] : [])+parent::get_fields($id, $action_key);
370
371 if($action_key == 'set'){
372 $fields = [
373 'name' => ['title'=>'名称', 'type'=>'text', 'class'=>'', 'required']
374 ]+($this->supports('slug') ? [
375 'slug' => ['title'=>'别名', 'type'=>'text', 'class'=>'', 'required']
376 ] : [])+($this->hierarchical && $this->levels !== 1 && $this->supports('parent') ? [
377 'parent' => ['title'=>'父级', 'options'=>['-1'=>'']+$this->get_options(apply_filters('taxonomy_parent_dropdown_args', ['exclude_tree'=>$id], $this->name, 'edit'))]
378 ] : [])+($this->supports('slug') ? [
379 'description' => ['title'=>'描述', 'type'=>'textarea']
380 ] : [])+$fields;
381 }
382
383 return $fields;
384 }
385
386 public function get_options($args=[]){
387 return array_column(wpjam_get_terms($args+['taxonomy'=>$this->name, 'hide_empty'=>0, 'format'=>'flat', 'parse'=>false]), 'name', 'term_id');
388 }
389
390 public function get_mapping($post_id){
391 $post = wpjam_try('wpjam_validate_post', $post_id, $this->mapping_post_type);
392 $data = ['name'=>$post->post_title, 'slug'=>$post->post_type.'-'.$post_id, 'taxonomy'=>$this->name];
393
394 if($term_id = get_post_meta($post_id, $this->query_key, true)){
395 if($term = get_term($term_id, $this->name)){
396 if($term->name != $data['name'] || $term->slug != $data['slug']){
397 WPJAM_Term::update($term_id, $data);
398 }
399
400 return $term_id;
401 }
402 }
403
404 return wpjam_tap(WPJAM_Term::insert($data), fn($term_id)=> update_post_meta($post_id, $this->query_key, $term_id));
405 }
406
407 public function dropdown(){
408 $args = ['taxonomy'=>$this->name, 'name'=>$this->query_key];
409 $value = $this->get_param($this->query_key);
410
411 if(is_null($value)){
412 $slug = ($var = $this->query_var) ? $this->get_param($var) : null;
413 $slug ??= $this->get_param('taxonomy') == $this->name ? $this->get_param('term') : '';
414 $term = $slug ? get_term_by('slug', $slug, $this->name) : '';
415 $value = $term ? $term->term_id : '';
416 }
417
418 if($this->hierarchical){
419 wp_dropdown_categories($args+[
420 'show_option_all' => $this->labels->all_items,
421 'show_option_none' => '没有设置',
422 'option_none_value' => 'none',
423 'selected' => $value,
424 'hierarchical' => true
425 ]);
426 }else{
427 echo wpjam_field($args+[
428 'value' => $value,
429 'type' => 'text',
430 'data_type' => 'taxonomy',
431 'filterable' => true,
432 'placeholder' => '请输�
433 �'.$this->title,
434 'title' => '',
435 'class' => ''
436 ]);
437 }
438 }
439
440 public function registered(){
441 $this->_jam && wpjam_init(fn()=> register_taxonomy($this->name, $this->object_type, $this->to_array()));
442 }
443
444 public static function filter_request($vars){
445 if(($struct = get_option('permalink_structure'))
446 && ($request = $GLOBALS['wp']->request)
447 && ($no_base = static::get_by('no_base', true))
448 && !isset($vars['module'])
449 && !is_admin()
450 ){
451 if(preg_match("#(.?.+?)/page/?([0-9]{1,})/?$#", $request, $matches)){
452 [, $request, $paged] = $matches;
453 }
454
455 if($GLOBALS['wp_rewrite']->use_verbose_page_rules){
456 if(($vars['error'] ?? '') == '404'){
457 $key = 'error';
458 }elseif(str_contains($struct, '/%postname%') && !empty($vars['name'])){
459 $key = 'name';
460 }elseif(!str_contains($request, '/')){
461 $type = array_find(['author', 'category'], fn($k)=> str_starts_with($struct, '/%'.$k.'%'));
462 $key = $type && !empty($vars[$type.'_name']) ? [$type.'_name', 'name'] : '';
463 }
464 }else{
465 $key = !empty($vars['pagename']) && !isset($_GET['page_id']) && !isset($_GET['pagename']) ? 'pagename' : '';
466 }
467
468 foreach(!empty($key) ? array_keys($no_base) : [] as $tax){
469 $name = is_taxonomy_hierarchical($tax) ? wp_basename($request) : $request;
470
471 if(array_find(wpjam_get_all_terms($tax), fn($term)=> $term->slug == $name)){
472 return ($tax == 'category' ? ['category_name'=>$name] : ['taxonomy'=>$tax, 'term'=>$name])+array_filter(['paged'=>$paged ?? 0])+wpjam_except($vars, $key);
473 }
474 }
475 }
476
477 return $vars;
478 }
479
480 public static function add_hooks(){
481 wpjam_init(fn()=> add_rewrite_tag('%term_id%', '([0-9]+)', 'term_id='));
482
483 wpjam_hook('query_vars', '+', ['term_id'], 11);
484
485 add_filter('pre_term_link', fn($link, $term)=> wpjam_get_taxonomy_setting($term->taxonomy, 'no_base') ? '%'.$term->taxonomy.'%' : str_replace('%term_id%', $term->term_id, $link), 1, 2);
486
487 add_filter('request', [self::class, 'filter_request']);
488 }
489 }
490
491 class WPJAM_Post extends WPJAM_Instance{
492 public function __get($key){
493 if(in_array($key, ['id', 'post_id'])){
494 return $this->id;
495 }elseif($key == 'views'){
496 return (int)$this->meta_get('views');
497 }elseif($key == 'viewable'){
498 return is_post_publicly_viewable($this->id);
499 }elseif($key == 'type_object'){
500 return wpjam_get_post_type_object($this->post_type);
501 }elseif($key == 'thumbnail'){
502 return $this->supports('thumbnail') ? get_the_post_thumbnail_url($this->id, 'full') : '';
503 }elseif($key == 'images'){
504 return $this->supports('images') ? array_values($this->meta_get('images') ?: []) : [];
505 }
506
507 return $this->builtin($key);
508 }
509
510 public function __call($method, $args){
511 if($method == 'get_type_setting'){
512 return $this->type_object->{$args[0]};
513 }elseif(in_array($method, ['get_taxonomies', 'supports'])){
514 return $this->type_object->$method(...$args);
515 }elseif(in_array($method, ['get_content', 'get_excerpt', 'get_first_image_url', 'get_thumbnail_url', 'get_images'])){
516 return ('wpjam_get_post_'.substr($method, 4))($this->post, ...$args);
517 }elseif($cb = ['get_terms'=>'get_the_terms', 'set_terms'=>'wp_set_post_terms', 'in_term'=>'is_object_in_term'][$method] ?? ''){
518 return $cb($this->id, ...$args);
519 }elseif($method === 'in_taxonomy'){
520 return is_object_in_taxonomy($this->post, ...$args);
521 }
522
523 return $this->call_dynamic_method($method, ...$args);
524 }
525
526 public function save($data){
527 if(array_find(['post_status', 'status'], fn($k)=> ($data[$k] ?? '') === 'publish')){
528 if($cb = wpjam_callback([$this, 'is_publishable'])){
529 $result = wpjam_catch($cb) ?: new WP_Error('cannot_publish', '不可发布');
530 }
531 }
532
533 return wpjam_then($result ?? true, fn()=> $data ? self::update($this->id, $data, false) : true);
534 }
535
536 public function set_status($status){
537 return $this->save(['post_status'=>$status]);
538 }
539
540 public function publish(){
541 return $this->set_status('publish');
542 }
543
544 public function unpublish(){
545 return $this->set_status('draft');
546 }
547
548 public function get_unserialized(){
549 return wpjam_unserialize($this->content, fn($fixed)=> $this->save(['content'=>$fixed])) ?: [];
550 }
551
552 public function parse_for_json($args=[]){
553 if($args && is_string($args) && in_array($args, ['date', 'modified'])){
554 $ts = get_post_timestamp($this->id, $args);
555 $prefix = $args == 'modified' ? $args.'_' : '';
556 $result = [$prefix.'timestamp'=>$ts, $prefix.'time'=>wpjam_human_time_diff($ts), $prefix.'date'=>wpjam_date('Y-m-d', $ts)];
557
558 return $result+($args == 'date' ? ['day'=>wpjam_human_date_diff($result['date'])] : []);
559 }
560
561 $args += self::get_default_args();
562 $query = $args['query'] ?? null;
563 $main = $query && $query->is_main_query();
564 $single = $query && ($query->is_single($this->id) || $query->is_page($this->id));
565 $rest = $single ? 'show_in_rest' : 'show_in_posts_rest';
566 $json = wpjam_pick($this, ['id', 'type', 'post_type', 'status', 'views'])+['icon'=>(string)$this->type_object->icon];
567 $json += $this->viewable ? ['name'=>urldecode($this->name), 'post_url'=>str_replace(home_url(), '', get_permalink($this->id))] : [];
568 $json += wpjam_fill(['title', 'excerpt'], fn($k)=> $this->supports($k) ? html_entity_decode(('get_the_'.$k)($this->id)) : '');
569 $json += ['thumbnail'=>$this->get_thumbnail_url($args['thumbnail_size'])];
570 $json += $this->supports('images') ? ['images'=>$this->get_images()] : [];
571 $json += ['user_id'=>(int)$this->author];
572 $json += $this->supports('author') ? ['author'=>wpjam_get_user($this->author)] : [];
573 $json += $this->parse_for_json('date')+$this->parse_for_json('modified');
574 $json += $this->password ? ['password_protected'=>true, 'password_required'=>post_password_required($this->id)] : [];
575 $json += $this->supports('page-attributes') ? ['menu_order'=>(int)$this->menu_order] : [];
576 $json += $this->supports('post-formats') ? ['format'=>get_post_format($this->id) ?: ''] : [];
577
578 if($main || $args['options_required']){
579 $json += array_reduce(wpjam_get_post_options($this->type, [$rest=>true]), fn($c, $o)=> $c+$o->prepare($this->id), []);
580 }
581
582 if($main || $args['taxonomy_required']){
583 $json += array_reduce($this->get_taxonomies([$rest=>true], 'names'), fn($c, $t)=> $c+[$t=>wpjam_get_terms(['terms'=>$this->get_terms($t), 'taxonomy'=>$t])], []);
584 }
585
586 if(($main && $single) || $args['content_required']){
587 if($this->supports('editor')){
588 $json += ['content'=>$this->get_content(), 'multipage'=>(bool)$GLOBALS['multipage']];
589 $json += $json['multipage'] ? ['numpages'=>$GLOBALS['numpages'], 'page'=>$GLOBALS['page']] : [];
590 }else{
591 $json += is_serialized($this->content) ? ['content'=>$this->get_unserialized()] : [];
592 }
593 }
594
595 $filter = $args['suppress_filter'] ? '' : ($main ? 'wpjam_post_json' : ($args['filter'] ?? ''));
596
597 return $filter ? apply_filters($filter, $json, $this->id, $args) : $json;
598 }
599
600 public function value_callback($field){
601 if($field == 'tax_input'){
602 return wpjam_fill($this->get_taxonomies('names'), fn($tax)=> array_column($this->get_terms($tax), 'term_id'));
603 }
604
605 return $this->post->$field ?? $this->meta_get($field);
606 }
607
608 public function update_callback($data, $defaults){
609 return wpjam_then(
610 $this->save(wpjam_pull($data, [...array_keys($this->data), 'tax_input'])),
611 fn($result)=> $data ? $this->meta_input($data, $defaults) : $result
612 );
613 }
614
615 public static function get_default_args(){
616 return [
617 'suppress_filter' => false,
618 'options_required' => true,
619 'taxonomy_required' => true,
620 'content_required' => false,
621 'thumbnail_size' => null
622 ];
623 }
624
625 public static function get_instance($post=null, $type=null, $wp_error=false){
626 return wpjam_then(
627 static::validate($post ?: get_post(), $type),
628 fn($v)=> self::instance($v->ID, fn($id)=> [wpjam_get_post_type_setting(get_post_type($id), 'model') ?: 'WPJAM_Post', 'create_instance']($id)),
629 fn($e)=> $wp_error ? $e : null
630 );
631 }
632
633 public static function validate($value, $type=null){
634 $type ??= static::get_current_post_type();
635
636 return ($post = $value ? self::get_post($value) : null)
637 ? ((!post_type_exists($post->post_type) || ($type && $type !== 'any' && !in_array($post->post_type, (array)$type)))
638 ? new WP_Error('invalid_post_type')
639 : $post
640 )
641 : new WP_Error('invalid_id');
642 }
643
644 public static function get($post){
645 if($data = self::get_post($post, ARRAY_A)){
646 $key = 'post_content';
647 $data = ['id'=>$data['ID']]+(is_serialized($data[$key]) ? [$key=>wpjam_unserialize($data[$key])] : [])+$data;
648 $data += wpjam_array($data, fn($k, $v)=> try_prefix($k, '-', 'post_') ? [$k, $v] : null);
649 }
650
651 return $data;
652 }
653
654 public static function update($post_id, $data, $validate=true){
655 return wpjam_then(
656 $validate ? static::validate($post_id) : null,
657 fn()=> parent::update($post_id, $data)
658 );
659 }
660
661 protected static function call_method($method, ...$args){
662 if($method == 'insert'){
663 $data = $args[0];
664 $type = array_unique(array_filter([$data['post_type'] ?? '', static::get_current_post_type()]));
665 $type = count($type) <= 1 ? (array_first($type) ?: 'post') : '';
666
667 $data['post_type'] = $type && post_type_exists($type) ? $type : wpjam_throw('invalid_post_type');
668 $data['post_status'] ??= current_user_can(get_post_type_object($type)->cap->publish_posts) ? 'publish' : 'draft';
669 $data['post_author'] ??= get_current_user_id();
670 $data['post_date'] ??= wpjam_date('Y-m-d H:i:s');
671
672 return wp_insert_post(wp_slash($data), true, true);
673 }elseif($method == 'update'){
674 return wp_update_post(wp_slash(['ID'=>$args[0]]+$args[1]), true, true);
675 }elseif($method == 'delete'){
676 return wp_delete_post($args[0], true) ?: wpjam_throw('delete_error', '删除失败');
677 }
678 }
679
680 protected static function sanitize_data($data, $post_id=0){
681 $key = 'post_content';
682 $data += wpjam_array(get_class_vars('WP_Post'), fn($k, $v)=> try_prefix($k, '-', 'post_') && isset($data[$k]) ? ['post_'.$k, $data[$k]] : null);
683
684 return (is_array($data[$key] ?? '') ? [$key=>serialize($data[$key])] : [])+$data
685 +(!$post_id && method_exists(static::class, 'is_publishable') ? ['post_status'=>'draft'] : []);
686 }
687
688 public static function get_by_ids($post_ids){
689 return array_map('get_post', array_filter(self::update_caches($post_ids)));
690 }
691
692 public static function update_caches($post_ids, $update_term_cache=false, $update_meta_cache=false){
693 if($post_ids = array_filter(wp_parse_id_list($post_ids))){
694 _prime_post_caches($post_ids, $update_term_cache, $update_meta_cache);
695
696 $group = wpjam_group(wp_cache_get_multiple($post_ids, 'posts'), fn($v)=> $v ? 1 : 0);
697
698 do_action('wpjam_deleted_ids', 'post', array_keys($group[0] ?? []));
699 do_action('wpjam_update_post_caches', ($group[1] ?? []), compact('update_term_cache', 'update_meta_cache'));
700
701 return $group[1] ?? [];
702 }
703
704 return [];
705 }
706
707 public static function get_post($post, $output=OBJECT, $filter='raw'){
708 return wpjam_tap(get_post($post, $output, $filter), fn($v)=> $post && is_numeric($post) && !$v && do_action('wpjam_deleted_ids', 'post', $post));
709 }
710
711 public static function get_current_post_type(){
712 if(static::class !== self::class){
713 return (WPJAM_Post_Type::get(static::class, 'model', self::class) ?: [])['name'] ?? null;
714 }
715 }
716
717 public static function get_path($args, $item=[]){
718 $type = $item['post_type'];
719
720 return $args === 'fields'
721 ? (get_post_type_object($type) ? [$type.'_id' => self::get_field(['post_type'=>$type, 'required'=>true])] : [])
722 : (($id = is_array($args) ? (int)($args[$type.'_id'] ?? 0) : $args)
723 ? ($item['platform'] == 'template' ? get_permalink($id) : str_replace('%post_id%', $id, $item['path']))
724 : new WP_Error('invalid_id', [wpjam_get_post_type_setting($type, 'title')])
725 );
726 }
727
728 public static function get_field($args){
729 $args['title'] ??= is_string($args['post_type'] ?? null) ? wpjam_get_post_type_setting($args['post_type'], 'title') : null;
730 $placeholder = '请输�
731 �'.$args['title'].'ID或�
732 输�
733 ��
734 �键字筛选';
735
736 return $args+compact('placeholder')+['type'=>'text', 'class'=>'all-options', 'data_type'=>'post_type'];
737 }
738
739 public static function with_field($method, $field, $value){
740 $type = $field->post_type;
741
742 if($method == 'validate'){
743 return (is_numeric($value) && wpjam_try(static::class.'::validate', $value, $type)) ? (int)$value : null;
744 }elseif($method == 'parse'){
745 return wpjam_get_post($value, ['post_type'=>$type, 'thumbnail_size'=>$field->size]);
746 }
747 }
748
749 public static function query_items($args){
750 if(wpjam_pull($args, 'data_type')){
751 return wpjam_get_posts($args+[
752 's' => $args['search'] ?? null,
753 'posts_per_page' => $args['number'] ?? 10,
754 'suppress_filters' => false,
755 ]);
756 }
757
758 $args['post_status'] = wpjam_pull($args, 'status') ?: 'any';
759 $args['post_type'] ??= static::get_current_post_type();
760 $args['posts_per_page'] ??= $args['number'] ?? 10;
761
762 return [
763 'items' => $GLOBALS['wp_query']->query($args),
764 'total' => $GLOBALS['wp_query']->found_posts
765 ];
766 }
767
768 public static function query_calendar($args){
769 return array_reduce($GLOBALS['wp_query']->query([
770 'posts_per_page' => -1,
771 'post_status' => wpjam_pull($args, 'status') ?: 'any'
772 ]+$args+[
773 'post_type' => static::get_current_post_type(),
774 'monthnum' => $args['month']
775 ]), fn($c, $p)=> wpjam_set($c, wpjam_at($p->post_date, ' ', 0).'[]', $p), []);
776 }
777
778 public static function get_views(){
779 if(!in_array(get_current_screen()->base, ['edit', 'upload'])){
780 $counts = array_filter((array)wp_count_posts(static::get_current_post_type()));
781 $views = ['all'=>['filter'=>['status'=>null, 'show_sticky'=>null], 'label'=>'�
782 �部', 'count'=>array_sum($counts)]];
783
784 return wpjam_reduce($counts, fn($c, $v, $k)=> $c+(($object = get_post_status_object($k)) && $object->show_in_admin_status_list ? [$k=>['filter'=>['status'=>$k], 'label'=>$object->label, 'count'=>$v]] : []), $views);
785 }
786 }
787
788 public static function filter_fields($fields, $id){
789 return ($id && !is_array($id) && !isset($fields['title']) && !isset($fields['post_title']) ? ['title'=>['title'=>wpjam_get_post_type_setting(get_post_type($id), 'title').'标题', 'type'=>'view', 'value'=>get_the_title($id)]] : [])+$fields;
790 }
791
792 public static function get_meta($post_id, ...$args){ // deprecated
793 return wpjam_get_metadata('post', $post_id, ...$args);
794 }
795
796 public static function update_meta($post_id, ...$args){ // deprecated
797 return wpjam_update_metadata('post', $post_id, ...$args);
798 }
799
800 public static function update_metas($post_id, $data, $meta_keys=[]){
801 return static::update_meta($post_id, $data, $meta_keys);
802 }
803 }
804
805 class WPJAM_Term extends WPJAM_Instance{
806 public function __get($key){
807 if($key == 'id'){
808 return $this->id;
809 }elseif($key == 'tax_object'){
810 return wpjam_get_taxonomy($this->taxonomy);
811 }elseif($key == 'object_type'){
812 return $this->tax_object->$key ?: [];
813 }elseif($key == 'level'){
814 return get_term_level($this->id);
815 }elseif($key == 'depth'){
816 return get_term_depth($this->id);
817 }elseif($key == 'link'){
818 return get_term_link($this->term);
819 }
820
821 return $this->builtin($key);
822 }
823
824 public function __call($method, $args){
825 if($method == 'get_tax_setting'){
826 return $this->tax_object->{$args[0]};
827 }elseif(in_array($method, ['get_taxonomies', 'supports'])){
828 return $this->tax_object->$method(...$args);
829 }elseif(in_array($method, ['set_object', 'add_object', 'remove_object'])){
830 $cb = 'wp_'.$method.'_terms';
831
832 return $cb(array_shift($args), [$this->id], $this->taxonomy, ...$args);
833 }elseif($method == 'is_object_in'){
834 return is_object_in_term($args[0], $this->taxonomy, $this->id);
835 }
836
837 return $this->call_dynamic_method($method, ...$args);
838 }
839
840 public function value_callback($field){
841 return $this->term->$field ?? $this->meta_get($field);
842 }
843
844 public function update_callback($data, $defaults){
845 return wpjam_then(
846 $this->save(wpjam_pull($data, ['name', 'parent', 'slug', 'description', 'alias_of'])),
847 fn($result)=> $data ? $this->meta_input($data, $defaults) : $result
848 );
849 }
850
851 public function save($data){
852 return $data ? self::update($this->id, $data, false) : true;
853 }
854
855 public function get_object_type(){
856 return $this->object_type;
857 }
858
859 public function get_thumbnail_url($size='full', $crop=1){
860 return wpjam_get_term_thumbnail_url($this->term, $size, $crop);
861 }
862
863 public function parse_for_json($args=[]){
864 $id = $this->id;
865 $tax = $this->taxonomy;
866 $json = ['id'=>$id, 'name'=>html_entity_decode($this->name)]+wpjam_pick($this, ['name', 'taxonomy', 'count', ...(is_taxonomy_viewable($tax) ? ['slug'] : []), ...(is_taxonomy_hierarchical($tax) ? ['parent'] : []), 'description']);
867
868 return apply_filters('wpjam_term_json', array_reduce(wpjam_get_term_options($tax), fn($c, $v)=> array_merge($c, $v->prepare($id)), $json), $id);
869 }
870
871 public static function get_instance($term, $tax=null, $wp_error=false){
872 return wpjam_then(
873 self::validate($term, $tax),
874 fn($v)=> self::instance($v->term_id, fn($id)=> [wpjam_get_taxonomy_setting(get_term_taxonomy($id), 'model') ?: 'WPJAM_Term', 'create_instance']($id)),
875 fn($e)=> $wp_error ? $e : null
876 );
877 }
878
879 public static function get($term){
880 return wpjam_then(
881 $term ? self::get_term($term, '', ARRAY_A) : [],
882 fn($data)=> $data ? $data+['id'=>$data['term_id']] : $data
883 );
884 }
885
886 public static function update($term_id, $data, $validate=true){
887 return wpjam_then(
888 $validate ? static::validate($term_id) : null,
889 fn()=> parent::update($term_id, $data)
890 );
891 }
892
893 protected static function call_method($method, ...$args){
894 if($method == 'insert'){
895 $data = $args[0];
896 $tax = array_unique(array_filter([wpjam_pull($data, 'taxonomy'), static::get_current_taxonomy()]));
897 $tax = count($tax) == 1 ? array_first($tax) : null;
898
899 return wp_insert_term(wp_slash(wpjam_pull($data, 'name')), $tax, wp_slash($data))['term_id'];
900 }elseif($method == 'update'){
901 $data = $args[1];
902 $tax = wpjam_pull($data, 'taxonomy') ?: get_term_field('taxonomy', $args[0]);
903
904 return wp_update_term($args[0], $tax, wp_slash($data));
905 }elseif($method == 'delete'){
906 return wp_delete_term($args[0], get_term_field('taxonomy', $args[0]));
907 }
908 }
909
910 public static function get_meta($term_id, ...$args){ // deprecated
911 return wpjam_get_metadata('term', $term_id, ...$args);
912 }
913
914 public static function update_meta($term_id, ...$args){ // deprecated
915 return wpjam_update_metadata('term', $term_id, ...$args);
916 }
917
918 public static function update_metas($term_id, $data, $meta_keys=[]){ // deprecated
919 return self::update_meta($term_id, $data, $meta_keys);
920 }
921
922 public static function get_by_ids($term_ids){
923 return self::update_caches($term_ids);
924 }
925
926 public static function update_caches($term_ids){
927 if($term_ids = array_filter(wp_parse_id_list($term_ids))){
928 _prime_term_caches($term_ids);
929
930 $group = wpjam_group(wp_cache_get_multiple($term_ids, 'terms'), fn($v)=> $v ? 1 : 0);
931
932 do_action('wpjam_deleted_ids', 'term', array_keys($group[0] ?? []));
933
934 return $group[1] ?? [];
935 }
936
937 return [];
938 }
939
940 public static function get_term($term, $tax='', $output=OBJECT, $filter='raw'){
941 return wpjam_tap(get_term($term, $tax, $output, $filter), fn($v)=> $term && is_numeric($term) && !$v && do_action('wpjam_deleted_ids', 'term', $term));
942 }
943
944 public static function get_current_taxonomy(){
945 if(static::class !== self::class){
946 return (WPJAM_Taxonomy::get(static::class, 'model', self::class) ?: [])['name'] ?? null;
947 }
948 }
949
950 public static function get_path($args, $item=[]){
951 $tax = $item['taxonomy'];
952 $key = wpjam_get_taxonomy_setting($tax, 'query_key');
953
954 return $args === 'fields'
955 ? ($key ? [$key=> self::get_field(['taxonomy'=>$tax, 'required'=>true])] : [])
956 : (($id = is_array($args) ? (int)wpjam_get($args, $key) : $args)
957 ? ($item['platform'] == 'template' ? get_term_link($id) : str_replace('%term_id%', $id, $item['path']))
958 : new WP_Error('invalid_id', [wpjam_get_taxonomy_setting($tax, 'title')])
959 );
960 }
961
962 public static function get_field($args=[]){
963 $object = isset($args['taxonomy']) && is_string($args['taxonomy']) ? wpjam_get_taxonomy($args['taxonomy']) : null;
964 $type = $args['type'] ?? '';
965 $title = $args['title'] ??= $object ? $object->title : null;
966 $args += ['data_type'=>'taxonomy'];
967
968 if($object && $object->hierarchical){
969 if(!$type && is_admin() && $object->levels > 1 && $object->selectable()){
970 $type = 'cascade';
971 }elseif(!$type || ($type == 'mu-text' && empty($args['item_type']))){
972 $type = (!is_admin() || $object->selectable()) ? ($type ? 'mu-' : '').'select' : $type;
973 }elseif($type == 'mu-text' && $args['item_type'] == 'select'){
974 $type = 'mu-select';
975 }
976 }
977
978 if(in_array($type, ['select', 'mu-select', 'cascade'])){
979 if(($k = 'show_option_all') && ($v = array_first(wpjam_pull($args, [$k, 'option_all']))) !== false){
980 $args[$k] = $v === true || is_null($v) ? '请选择' : $v;
981 }
982
983 return ['type'=>$type]+($type == 'cascade' ? ['filter_key'=>'parent'] : ['options'=> fn()=> $object->get_options()])+$args;
984 }
985
986 return $args+['type'=>'text', 'class'=>'all-options', 'placeholder'=>'请输�
987 �'.$title.'ID或�
988 输�
989 ��
990 �键字筛选'];
991 }
992
993 public static function with_field($method, $field, $value){
994 $tax = $field->taxonomy;
995
996 if($method == 'render'){
997 $terms = get_terms(['taxonomy'=>$tax, 'hide_empty'=>0]);
998 $values = $value ? array_reverse([$value, ...get_ancestors($value, $tax, 'taxonomy')]) : [];
999
1000 for($i = 0; $i < wpjam_get_taxonomy_setting($tax, 'levels'); $i++){
1001 $parent = $i ? ($values[$i-1] ?? null) : 0;
1002 $options[] = is_null($parent) ? [] : array_column(wp_list_filter($terms, ['parent'=>$parent]), 'name', 'term_id');
1003 }
1004
1005 return ['value'=>$values, 'options'=>$options];
1006 }elseif($method == 'validate'){
1007 if(is_array($value)){
1008 $value = array_first(wpjam_sort(array_filter($value), 'kr')) ?: 0;
1009 }
1010
1011 if(is_numeric($value)){
1012 return !$value || wpjam_try('get_term', $value, $tax) ? (int)$value : null;
1013 }
1014
1015 $result = term_exists($value, $tax);
1016
1017 return $result ? (is_array($result) ? $result['term_id'] : $result) : ($field->creatable ? wpjam_try('WPJAM_Term::insert', ['name'=>$value, 'taxonomy'=>$tax]) : null);
1018 }elseif($method == 'parse'){
1019 return wpjam_get_term($value, $tax);
1020 }
1021 }
1022
1023 public static function query_items($args){
1024 if(wpjam_pull($args, 'data_type')){
1025 return array_values(get_terms($args+['number'=>(isset($args['parent']) ? 0 : 10), 'hide_empty'=>false]));
1026 }
1027
1028 $defaults = ['hide_empty'=>false, 'taxonomy'=>static::get_current_taxonomy()];
1029
1030 return ['items'=>get_terms($args+$defaults), 'total'=>wp_count_terms($defaults)];
1031 }
1032
1033 public static function validate($id, $tax=null){
1034 $tax ??= self::get_current_taxonomy();
1035
1036 return wpjam_then(self::get_term($id), fn($v)=> (!$v || !($v instanceof WP_Term))
1037 ? new WP_Error('invalid_term_id')
1038 : (($tax && $tax !== 'any' && !in_array($v->taxonomy, (array)$tax)) ? new WP_Error('invalid_taxonomy') : $v)
1039 );
1040 }
1041
1042 public static function filter_fields($fields, $id){
1043 return ($id && !is_array($id) && !isset($fields['name']) ? ['name'=>['title'=>wpjam_get_taxonomy_setting(get_term_field('taxonomy', $id), 'title'), 'type'=>'view', 'value'=>get_term_field('name', $id)]] : [])+$fields;
1044 }
1045 }
1046
1047 class WPJAM_User extends WPJAM_Instance{
1048 public function __get($key){
1049 if(in_array($key, ['id', 'user_id'])){
1050 return $this->id;
1051 }elseif($key == 'role'){
1052 return array_first($this->roles);
1053 }elseif($key === 'data'){
1054 return get_userdata($this->id);
1055 }
1056
1057 return $this->builtin($key);
1058 }
1059
1060 public function value_callback($field){
1061 return $this->$field;
1062 }
1063
1064 public function save($data){
1065 return $data ? self::update($this->id, $data) : true;
1066 }
1067
1068 public function parse_for_json($size=96){
1069 return apply_filters('wpjam_user_json', [
1070 'id' => $this->id,
1071 'nickname' => $this->nickname,
1072 'name' => $this->display_name,
1073 'display_name' => $this->display_name,
1074 'avatar' => get_avatar_url($this->user, $size),
1075 ], $this->id);
1076 }
1077
1078 public function add_role($role, $blog_id=0){
1079 return $role ? wpjam_call_for_blog($blog_id, fn()=> $this->roles && !in_array($role, $this->roles)
1080 ? new WP_Error('error', '你已有权限,如果需要更改权限,请联系管理员直接修改。')
1081 : wpjam_tap($this, fn()=> $this->user->add_role($role))) : $this;
1082 }
1083
1084 public function login(){
1085 wp_set_auth_cookie($this->id, true, is_ssl());
1086 wp_set_current_user($this->id);
1087 do_action('wp_login', $this->user_login, $this->user);
1088
1089 return $this;
1090 }
1091
1092 public static function get_instance($id, $wp_error=false){
1093 return wpjam_then(
1094 self::validate($id),
1095 fn($v)=> self::instance($v->ID, fn($id)=> new self($id)),
1096 fn($e)=> $wp_error ? $e : null
1097 );
1098 }
1099
1100 public static function validate($user_id){
1101 return ($user_id && ($user = self::get_user($user_id)) && ($user instanceof WP_User)) ? $user : new WP_Error('invalid_user_id');
1102 }
1103
1104 public static function update_caches($ids){
1105 $ids = array_filter(wp_parse_id_list($ids));
1106
1107 return $ids && (cache_users($ids) || true) ? array_map('get_userdata', $ids) : [];
1108 }
1109
1110 public static function get_by_ids($ids){
1111 return self::update_caches($ids);
1112 }
1113
1114 public static function get_user($user){
1115 return $user && is_numeric($user) ? wpjam_tap(get_userdata($user), fn($v)=> !$v && do_action('wpjam_deleted_ids', 'user', $user)) : $user;
1116 }
1117
1118 public static function get_authors($args=[]){
1119 return get_users(array_merge($args, ['capability'=>'edit_posts']));
1120 }
1121
1122 public static function get_path($args, $item=[]){
1123 return $args === 'fields'
1124 ? ['author' => ['type'=>'select', 'options'=>fn()=> wp_list_pluck(WPJAM_User::get_authors(), 'display_name', 'ID')]]
1125 : (($id = is_array($args) ? (int)wpjam_get($args, 'author') : $args)
1126 ? ($item['platform'] == 'template' ? get_author_posts_url($id) : str_replace('%author%', $id, $item['path']))
1127 : new WP_Error('invalid_author', ['作�
1128 '])
1129 );
1130 }
1131
1132 public static function options_callback($field){
1133 return wp_list_pluck(self::get_authors(), 'display_name', 'ID');
1134 }
1135
1136 public static function get($id){
1137 return ($user = get_userdata($id)) ? $user->to_array() : [];
1138 }
1139
1140 protected static function call_method($method, ...$args){
1141 if($method == 'create'){
1142 $args = $args[0]+[
1143 'user_pass' => wp_generate_password(12, false),
1144 'user_login' => '',
1145 'user_email' => '',
1146 'nickname' => '',
1147 // 'avatarurl' => '',
1148 ];
1149
1150 if(!wpjam_pull($args, 'users_can_register', get_option('users_can_register'))){
1151 return new WP_Error('registration_closed', '用户注册�
1152 �闭,请联系管理员手动添加!');
1153 }
1154
1155 if(empty($args['user_email'])){
1156 return new WP_Error('empty_user_email', '用户的邮箱不能为空。');
1157 }
1158
1159 $args['user_login'] = preg_replace('/\s+/', '', sanitize_user($args['user_login'], true));
1160
1161 $lock_key = $args['user_login'] ? $args['user_login'].'_register_lock' : '';
1162
1163 if($lock_key && wpjam_lock($lock_key, 5, 'users')){
1164 return new WP_Error('error', '该用户名正在注册中,请稍后再试!');
1165 }
1166
1167 $data = wpjam_pick($args, ['user_login', 'user_pass', 'user_email', 'role']);
1168 $data += $args['nickname'] ? ['nickname'=>$args['nickname'], 'display_name'=>$args['nickname']] : [];
1169 $id = static::insert($data);
1170
1171 if($lock_key){
1172 wpjam_lock($lock_key, -1, 'users');
1173 }
1174
1175 return wpjam_then($id, fn()=> static::get_instance($id));
1176 }elseif($method == 'insert'){
1177 return wp_insert_user(wp_slash($args[0]));
1178 }elseif($method == 'update'){
1179 return wp_update_user(wp_slash(array_merge($args[1], ['ID'=>$args[0]])));
1180 }elseif($method == 'delete'){
1181 return wp_delete_user($args[0]);
1182 }
1183 }
1184
1185 public static function create($args){
1186 return wpjam_call_for_blog(wpjam_get($args, 'blog_id'), fn()=> static::call_method('create', $args));
1187 }
1188
1189 public static function query_items($args){
1190 if(wpjam_pull($args, 'data_type')){
1191 return get_users(array_merge($args, ['search'=> !empty($args['search']) ? '*'.$args['search'].'*' : '']));
1192 }
1193 }
1194
1195 public static function filter_fields($fields, $id){
1196 if($id && !is_array($id)){
1197 $object = self::get_instance($id);
1198 $fields = array_merge(['name'=>['title'=>'用户', 'type'=>'view', 'value'=>$object->display_name]], $fields);
1199 }
1200
1201 return $fields;
1202 }
1203 }
1204
1205 class WPJAM_Bind extends WPJAM_Register{
1206 public function __construct($type, $appid){
1207 parent::__construct($type.':'.$appid, [
1208 'type' => $type,
1209 'appid' => $appid,
1210 'bind_key' => wpjam_join('_', $type, $appid),
1211 'domain' => $type == 'phone' ? 'phone.sms' : $appid.'.'.$type
1212 ]);
1213 }
1214
1215 public function get_appid(){
1216 return $this->appid;
1217 }
1218
1219 public function get_openid($meta_type, $object_id){
1220 return get_metadata($meta_type, $object_id, $this->bind_key, true);
1221 }
1222
1223 public function update_openid($meta_type, $object_id, $openid){
1224 if(!$openid){
1225 return delete_metadata($meta_type, $object_id, $this->bind_key);
1226 }
1227
1228 return update_metadata($meta_type, $object_id, $this->bind_key, $openid);
1229 }
1230
1231 public function bind_openid($meta_type, $object_id, $openid){
1232 $bound = ($current = $this->get_openid($meta_type, $object_id)) && $current != $openid;
1233
1234 if($bound = $bound ?: wpjam_then($this->get_by_openid($meta_type, $openid), fn($v)=> $v && $v->id != $object_id)){
1235 return wpjam_then($bound, new WP_Error('is_bound', '已绑定�
1236 �他账号,请�
1237 �解绑再试!'));
1238 }
1239
1240 $this->update_value($openid, $meta_type.'_id', $object_id);
1241 $this->update_openid($meta_type, $object_id, $openid);
1242
1243 return $openid;
1244 }
1245
1246 public function unbind_openid($meta_type, $object_id, $openid=null){
1247 $openid = $openid ?: $this->get_openid($meta_type, $object_id);
1248
1249 if($openid = $openid ?: wpjam_call_method($this, 'get_openid_by', $meta_type.'_id', $object_id)){
1250 $this->update_openid($meta_type, $object_id, '');
1251 $this->update_value($openid, $meta_type.'_id', 0);
1252 }
1253
1254 return $openid;
1255 }
1256
1257 public function get_by_openid($meta_type, $openid){
1258 if(!$this->get_value($openid)){
1259 return new WP_Error('invalid_openid');
1260 }
1261
1262 $cb = 'wpjam_get_'.$meta_type.'_object';
1263 $object = wpjam_call($cb, $this->get_value($openid, $meta_type.'_id'));
1264 $object ??= ($meta = wpjam_get_by_meta($meta_type, $this->bind_key, $openid)) ? wpjam_call($cb, array_first($meta)[$meta_type.'_id']) : null;
1265
1266 return $object ?: (($meta_type == 'user' && ($user_id = username_exists($openid))) ? wpjam_get_user($user_id, 'object') : null);
1267 }
1268
1269 public function get_by_user_email($meta_type, $email){
1270 if($email && try_suffix($email, '-', '@'.$this->domain)){
1271 return $this->get_value($email, $meta_type.'_id');
1272 }
1273 }
1274
1275 protected function get_value($openid, $key=null){
1276 return wpjam_get(method_exists($this, 'get_user') ? $this->get_user($openid) : ['openid'=>$openid], $key ?: null);
1277 }
1278
1279 protected function update_value($openid, $key, $value){
1280 return method_exists($this, 'update_user') ? $this->update_user($openid, [$key=>$value]) : true;
1281 }
1282
1283 public function get_user_email($openid){
1284 return $openid.'@'.$this->domain;
1285 }
1286
1287 public function get_email($openid){
1288 return $this->get_user_email($openid);
1289 }
1290
1291 public function get_avatarurl($openid){
1292 return $this->get_value($openid, 'avatarurl');
1293 }
1294
1295 public function get_nickname($openid){
1296 return $this->get_value($openid, 'nickname');
1297 }
1298
1299 public function get_unionid($openid){
1300 return $this->get_value($openid, 'unionid');
1301 }
1302
1303 public function get_phone_data($openid){
1304 return ($phone = $this->get_value($openid, 'phone')) ? ['phone'=>$phone, 'country_code'=>$this->get_value($openid, 'country_code') ?: 86] : [];
1305 }
1306 }
1307
1308 class WPJAM_User_Signup extends WPJAM_Register{
1309 public function __call($method, $args){
1310 return [wpjam_get_bind($this->type, $this->appid), $method](...(str_ends_with($method, '_openid') ? ['user', ...$args] : $args));
1311 }
1312
1313 public function signup($data, $args=null){
1314 if(is_array($data)){
1315 $by = $this->by;
1316 $args ??= ($data['args'] ?? '') ?: [];
1317 $key = $by == 'code' ? '' : ($data[$by] ?? '');
1318 $user = apply_filters('wpjam_user_signup', null, $by, $key, $data['code'] ?? '') ?: $this->signup($this->verify($data), $args);
1319
1320 return wpjam_tap($user, null, fn()=> do_action('wpjam_user_signup_failed', $by, $key, $user));
1321 }
1322
1323 $openid = $data;
1324 $args = apply_filters('wpjam_user_signup_args', $args ?? [], $this->type, $this->appid, $openid);
1325 $user = wpjam_then($openid, fn()=> wpjam_then(
1326 $this->get_by_openid($openid),
1327 fn($user)=> $user ? $user->add_role($args['role'] ?? '', $args['blog_id'] ?? 0) : WPJAM_User::create([
1328 'user_login' => $openid,
1329 'user_email' => $this->get_user_email($openid),
1330 'nickname' => $this->get_nickname($openid)
1331 ]+$args)
1332 ));
1333
1334 return wpjam_tap($user, fn()=> $this->bind($openid, $user->id) && do_action('wpjam_user_signuped', $user->login()->data, $args));
1335 }
1336
1337 public function bind($data, $user_id=null){
1338 $user = wpjam_get_user($user_id ?? get_current_user_id(), 'object');
1339 $openid = is_array($data) ? $this->verify($data) : $data;
1340 $openid = wpjam_then($openid, fn()=> $this->bind_openid($user->id, $openid));
1341
1342 if(!wpjam_if_error($openid, '')){
1343 if(($avatarurl = $this->get_avatarurl($openid)) && $avatarurl !== $user->avatarurl){
1344 $user->meta_input('avatarurl', $avatarurl);
1345 }
1346
1347 if(($nickname = $this->get_nickname($openid)) && $nickname !== $user->nickname){
1348 $user->save(['nickname'=>$nickname, 'display_name'=>$nickname]);
1349 }
1350 }
1351
1352 return $openid;
1353 }
1354
1355 public function unbind($user_id=null){
1356 return $this->unbind_openid($user_id ?? get_current_user_id());
1357 }
1358
1359 public function verify($data){
1360 return $this->qrcode
1361 ? wpjam_then($this->qrcode->verify($data['scene'], $data['code']), fn($qrcode)=> $qrcode['openid'])
1362 : $this->call('verify', $data);
1363 }
1364
1365 public function get_fields($action='login', $for='admin'){
1366 if($action == 'bind' && ($openid = $this->get_openid(get_current_user_id()))){
1367 $view = wpjam_join("<br />", [
1368 ($avatar = $this->get_avatarurl($openid)) ? '<img src="'.str_replace('/132', '/0', $avatar).'" width="272" />' : '',
1369 ($nickname = $this->get_nickname($openid)) ? '<strong>'.$nickname.'</strong>' : ''
1370 ]);
1371
1372 return [
1373 'view' => ['type'=>'view', 'title'=>'已绑定', 'value'=>($view ?: $openid)],
1374 'action' => ['type'=>'hidden', 'value'=>'unbind'],
1375 ];
1376 }
1377
1378 $fields = $this->qrcode
1379 ? wpjam_then($this->qrcode->get_fields($action), fn($v)=> wpjam_set($v, 'qrcode[title]', $this->title))
1380 : ((is_callable($this->fields) ? $this->call('fields', $action, $for) : $this->fields) ?: []);
1381
1382 return wpjam_then($fields, $fields+['action' => ['type'=>'hidden', 'value'=>$action]]);
1383 }
1384
1385 public function get_attr($action='login', $for=''){
1386 return wpjam_then($this->get_fields($action, $for), fn($fields)=> array_merge(
1387 $action == 'bind' ? ['submit_text'=>$fields['action']['value'] == 'unbind' ? '解除绑定' : '立刻绑定'] : [],
1388 $for != 'admin' ? wpjam_ajax($this->name.'-signup')->to_array() : [],
1389 ['fields'=>$for != 'admin' ? wpjam_fields($fields)->render(['wrap_tag'=>'p']) : $fields]
1390 ));
1391 }
1392
1393 public function get_form(){
1394 return $this->get_attr('bind', 'admin')+[
1395 'callback' => [$this, 'callback'],
1396 'capability' => 'read',
1397 'validate' => true,
1398 'response' => 'redirect'
1399 ];
1400 }
1401
1402 public function callback($data){
1403 $action = wpjam_pull($data, 'action');
1404
1405 return try_prefix($action, '-', 'get-')
1406 ? $this->get_attr($action)
1407 : wpjam_then(wpjam_catch([$this, $action == 'login' ? 'signup' : $action], ...($action == 'unbind' ? [] : [$data])), true);
1408 }
1409
1410 public function registered(){
1411 wpjam_ajax($this->name.'-signup', [
1412 'nopriv' => true,
1413 'callback' => [$this, 'callback'],
1414 'nonce_action' => fn($data)=> str_starts_with($data['action'] ?? '', 'get-') ? '' : $this->name.'-signup'
1415 ]);
1416 }
1417
1418 public static function create($type, $args){
1419 if(wpjam_get_bind($type, $args['appid'] ?? '')){
1420 $model = wpjam_pull($args, 'model') ?: 'WPJAM_User_Signup';
1421 $model = is_object($model) ? get_class($model) : $model; // �
1422 �容
1423
1424 return self::register(new $model($type, $args+['type'=>$type]));
1425 }
1426 }
1427
1428 public static function on_admin_init(){
1429 $objects = self::get_registereds();
1430 $tabs = wpjam_array($objects, fn($k, $v)=> $v->bind ? [$k, [
1431 'title' => $v->title,
1432 'capability' => 'read',
1433 'function' => 'form',
1434 'form' => [$v, 'get_form']
1435 ]] : null);
1436
1437 $tabs && wpjam_add_menu_page([
1438 'parent' => 'users',
1439 'menu_slug' => 'wpjam-bind',
1440 'menu_title' => '账号绑定',
1441 'order' => 20,
1442 'capability' => 'read',
1443 'function' => 'tab',
1444 'tabs' => $tabs
1445 ]);
1446
1447 $objects && wpjam_add_admin_load([
1448 'base' => 'users',
1449 'callback' => fn()=> wpjam_register_list_table_column('openid', [
1450 'title' => '绑定账号',
1451 'order' => 20,
1452 'callback' => fn($user_id)=> wpjam_join('<br /><br />', wpjam_map($objects, fn($v)=> ($openid = $v->get_openid($user_id)) ? $v->title.'<br />'.$openid : ''))
1453 ])
1454 ]);
1455 }
1456
1457 public static function on_login_init(){
1458 $action = ($_REQUEST['action'] ?? '') ?: 'login';
1459 $type = $_REQUEST[$action.'_type'] ?? '';
1460
1461 if(in_array($action, ['login', 'bind']) && ($objects = self::get_registereds([$action=>true]))){
1462 if($action == 'login'){
1463 $type = $type ?: apply_filters('wpjam_default_login_type', $action);
1464 $type = $type ?: ($_SERVER['REQUEST_METHOD'] == 'POST' ? 'login' : array_key_first($objects));
1465
1466 if(isset($objects[$type])){
1467 wpjam_call($objects[$type]->login_action);
1468 }
1469
1470 if(empty($_COOKIE[TEST_COOKIE])){
1471 $_COOKIE[TEST_COOKIE] = 'WP Cookie check';
1472 }
1473
1474 $objects['login'] = '使用账号和密码登录';
1475 }else{
1476 is_user_logged_in() ? wpjam_hook('login_display_language_dropdown', false) : wp_die('登录之后才能执行绑定操作!');
1477 }
1478
1479 $type = $type && isset($objects[$type]) ? $type : array_key_first($objects);
1480 $tag = wpjam_tag('p', ['class'=>'types', 'data'=>['action'=>$action]])->append(wpjam_map($objects, fn($v, $k)=>['a', [
1481 'class' => $type == $k ? 'current' : '',
1482 'data' => ['type'=>$k]+($k == 'login' ? [] : ['action'=>$k.'-signup', 'data'=>['action'=>'get-'.$action]])
1483 ], $k == 'login' ? $v : ($action == 'bind' ? '绑定'.$v->title : $v->login_title)]));
1484
1485 wp_enqueue_script('wpjam-login', wpjam_url(dirname(__DIR__).'/static/login.js'), ['wpjam-ajax']);
1486
1487 wpjam_echo($tag, 'login_form');
1488 }
1489
1490 wp_add_inline_style('login', join("\n", [
1491 '.login .message, .login #login_error{margin-bottom: 0;}',
1492 '.code_wrap label:last-child{display:flex;}',
1493 '.code_wrap input.button{margin-bottom:10px;}',
1494 '.login form .input, .login input[type=password], .login input[type=text]{font-size:20px; margin-bottom:10px;}',
1495
1496 'p.types{line-height:2; float:left; clear:left; margin-top:10px;}',
1497 'p.types a{text-decoration: none; display:block;}',
1498 'p.types a.current{display:none;}',
1499 'div.fields{margin-bottom:10px;}',
1500 ]));
1501 }
1502
1503 public static function add_hooks(){
1504 if(wp_using_ext_object_cache()){
1505 add_action('login_init', [self::class, 'on_login_init']);
1506 add_action('wpjam_admin_init', [self::class, 'on_admin_init']);
1507 }
1508 }
1509 }