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

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

1,177 lines 36.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 class WPJAM_API{
3 private $data = [];
4
5 public function __invoke($field, ...$args){
6 if(!$field){
7 return $this;
8 }
9
10 if(method_exists($this, $field)){
11 return $this->$field(...$args);
12 }
13
14 if(try_suffix($field, '-', '[]')){
15 $method = $args ? (count($args) <= 2 && is_null(array_last($args)) ? 'delete' : 'add') : 'get';
16 }else{
17 $method = $args && (count($args) > 1 || is_array($args[0])) ? 'set' : 'get';
18 }
19
20 return $this->$method($field, ...$args);
21 }
22
23 public function add($field, $key, ...$args){
24 [$key, $item] = $args ? [$key, $args[0]] : [null, $key];
25
26 if(isset($key) && !str_ends_with($key, '[]') && $this->get($field, $key) !== null){
27 return new WP_Error('invalid_key', ''.$key.'」已存在,无法添加');
28 }
29
30 return $this->set($field, $key, $item);
31 }
32
33 public function set($field, $key, ...$args){
34 $this->data[$field] = is_array($key) ? array_merge(($args && $args[0]) ? $this->get($field) : [], $key) : wpjam_set($this->get($field), $key ?? '[]', ...$args);
35
36 return is_array($key) ? $key : $args[0];
37 }
38
39 public function delete($field, ...$args){
40 if($args){
41 return $this->data[$field] = wpjam_except($this->get($field), $args[0]);
42 }
43
44 unset($this->data[$field]);
45 }
46
47 public function get($field, ...$args){
48 return $args ? wpjam_get($this->get($field), ...$args) : ($this->data[$field] ?? []);
49 }
50
51 public static function get_instance(){
52 static $object;
53 return $object ??= new self();
54 }
55
56 public static function __callStatic($method, $args){
57 $function = 'wpjam_'.$method;
58
59 if(function_exists($function)){
60 return $function(...$args);
61 }
62 }
63 }
64
65 class WPJAM_Callback{
66 private $reflections = [];
67 private $annotations = [];
68
69 public function __invoke($cb, ...$args){
70 if(is_null($cb)){
71 return $this;
72 }
73
74 if(is_string($cb)){
75 if(in_array($cb, ['', 'try', 'catch', 'method'], true)){
76 return $this->call($cb, ...$args);
77 }
78
79 if(method_exists($this, $cb)){
80 return $this->$cb(...$args);
81 }
82 }
83
84 return $this->parse($args ? 'try' : '', $cb, ...$args);
85 }
86
87 public function call($type, $cb, ...$args){
88 $res = $this->parse($type, $cb, $args);
89
90 if(is_array($res)){
91 [$cb, $args] = $res;
92
93 return $cb(...$args);
94 }
95
96 return $res;
97 }
98
99 public function parse($type, $cb, ...$args){
100 if(is_string($cb) && preg_match('/::|->/', $cb, $m)){
101 $sep = $m[0];
102 $static = $sep == '::';
103 $cb = explode($sep, $cb, 2);
104 }elseif(!is_array($cb)){
105 if($type == 'method'){
106 $cb = [$cb, array_shift($args[0])];
107 }
108 }
109
110 if(is_array($cb)){
111 if(!$cb[0] || (is_string($cb[0]) && !class_exists($cb[0]))){
112 return $this->invalid($type, $cb[0], 'model');
113 }
114
115 $exists = $cb[1] && method_exists(...$cb);
116
117 if($type == 'method' && !$exists){
118 return;
119 }
120 }else{
121 $exists = $cb && is_callable($cb);
122 }
123
124 if(!$args){
125 return $exists ? $cb : null;
126 }
127
128 $args = $args[0];
129
130 if(is_array($cb) && is_string($cb[0])){
131 $sep ??= '::';
132 $static ??= $exists ? $this->reflection($cb, 'isStatic') : ($cb[1] ? method_exists($cb[0], '__callStatic') : null);
133
134 if(is_null($static) || (!$exists && !method_exists($cb[0], '__call'.($static ? 'Static' : '')))){
135 return $this->invalid($type, implode($sep, $cb));
136 }
137
138 if(!$static){
139 if($cb[1] == 'value_callback' && count($args) == 2){
140 $args = array_reverse($args);
141 }
142
143 $ins = [$cb[0], 'get_instance'];
144 $num = $this->reflection($ins, 'NumberOfRequiredParameters');
145
146 if(is_null($num) || count($args) < $num){
147 return $this->invalid($type, implode($sep, $cb));
148 }
149
150 $cb[0] = $ins(...array_splice($args, 0, $num ?: 1));
151
152 if(!$cb[0]){
153 return $this->invalid($type, $cb[0], 'id');
154 }
155 }
156
157 $cb = ($public ?? true) ? $cb : $this->reflection($cb, 'Closure')($static ? null : $cb[0]);
158 }else{
159 if(!is_callable($cb)){
160 return $this->invalid($type, $cb);
161 }
162 }
163
164 return [$cb, $args];
165 }
166
167 private function invalid($type, $msg, $code='callback'){
168 if(in_array($type, ['try', 'catch'])){
169 $code = 'invalid_'.$code;
170
171 return $type == 'catch' ? new WP_Error($code, [$msg]) : wpjam_throw($code, [$msg]);
172 }
173 }
174
175 public function value_callback($args, $name){
176 $path = (array)$name;
177 $name = array_shift($path);
178 $id = $args['id'] ?? null;
179 $cb = $args['value_callback'] ?? '';
180 $mt = $id ? ($args['meta_type'] ?? '') : '';
181
182 if(!$cb && ($model = $args['model'] ?? '')){
183 if($id && $path && $name == 'meta_input' && ($mt = $mt ?: $this->call('method', $model.'::get_meta_type'))){
184 $name = array_shift($path);
185 }else{
186 $cb = $this->parse('', [$model, 'value_callback']);
187 }
188 }
189
190 foreach([$cb, $mt] as $i => $t){
191 $value = $t ? wpjam_if_error(($i === 0 ? $this->call('', $t, $name, $id) : wpjam_get_metadata($t, $id, $name)), null) : null;
192 $value = $path ? wpjam_get($value ?: [], $path) : $value;
193
194 if(isset($value)){
195 return $value;
196 }
197 }
198 }
199
200 public function render($cb){
201 return wpautop(is_array($cb) ? (is_object($cb[0]) ? get_class($cb[0]).'->' : $cb[0].'::').(string)$cb[1] : (is_object($cb) ? get_class($cb) : $cb));
202 }
203
204 public function uniqid($cb){
205 return _wp_filter_build_unique_id(null, $cb, null);
206 }
207
208 public function reflection($cb, ...$args){
209 if($cb === 'class'){
210 $type = 'class';
211 $class = array_shift($args);
212 $key = class_exists($class) ? strtolower($class) : null;
213 }else{
214 $type = 'cb';
215 $cb = $this->parse('try', is_array($cb) && !is_string($cb[0]) ? [get_class($cb[0]), $cb[1]] : $cb);
216 $key = $cb ? $this->uniqid($cb) : null;
217 }
218
219 if($key){
220 $param = $args ? array_shift($args) : '';
221 $ref = $this->reflections[$type][$key] ??= $type == 'class' ? new ReflectionClass($class) : (is_array($cb) ? new ReflectionMethod(...$cb) : new ReflectionFunction($cb));
222
223 return $param ? [$ref, (array_find(['get', 'is', 'has', 'in'], fn($v)=> str_starts_with($param, $v)) ? '' : 'get').$param](...$args) : $ref;
224 }
225 }
226
227 public function annotation($class, $key=''){
228 $data = $this->annotations[strtolower($class)] ??= (function($ref){
229 if(method_exists($ref, 'getAttributes')){
230 foreach($ref->getAttributes() as $attr){
231 $k = $attr->getName();
232 $v = $attr->getArguments();
233 $v = ($v && wp_is_numeric_array($v) && ($k == 'config' ? is_array($v[0]) : count($v) == 1)) ? $v[0] : $v;
234
235 $data[$k] = $v ?: null;
236 }
237 }elseif(preg_match_all('/@([a-z0-9_]+)\s+([^\r\n]*)/i', ($ref->getDocComment() ?: ''), $matches, PREG_SET_ORDER)){
238 foreach($matches as $m){
239 $k = $m[1];
240 $v = trim($m[2]) ?: null;
241
242 $data[$k] = ($v && $k == 'config') ? wp_parse_list($v) : $v;
243 }
244 }
245
246 $data['config'] = wpjam_array($data['config'] ?? [], fn($k, $v)=> is_numeric($k) ? (str_contains($v, '=') ? explode('=', $v, 2) : [$v, true]) : [$k, $v]);
247
248 return $data;
249 })($this->reflection('class', $class));
250
251 return wpjam_get($data, $key ?: null);
252 }
253
254 public static function get_instance(){
255 static $object;
256 return $object ??= new self();
257 }
258 }
259
260 class WPJAM_Query{
261 private $type;
262
263 private function __construct($type){
264 $this->type = $type;
265 }
266
267 public function is($query, ...$args){
268 if($query->is_main_query()){
269 if($args){
270 return array_any(wp_parse_list(array_shift($args)), fn($t)=> [$query, 'is_'.$t](...$args));
271 }
272
273 return $query->is_front_page() ? 'home' : (array_find(['feed', 'author', 'category', 'tag', 'tax', 'post_type_archive', 'search', 'date', 'archive', '404', 'page', 'single', 'attachment'], fn($t)=> [$query, 'is_'.$t]()) ?: true);
274 }
275 }
276
277 public function query($vars, &$args=[]){
278 if($this->type == 'term'){
279 $number = (int)wpjam_pull($vars, 'number');
280 $paged = wpjam_pull($vars, 'paged') ?: 1;
281 $args += wpjam_pull($vars, ['format', 'parse']);
282 $terms = $this->parse($vars, $args);
283
284 return $terms && $number ? array_slice($terms, $number * ($paged-1), $number) : $terms;
285 }
286
287 if(!empty($vars['related_query'])){
288 $post = get_post(wpjam_pull($vars, 'post') ?? get_the_ID());
289 $type = get_post_type($post);
290 $tt_ids = [];
291
292 foreach($post ? get_object_taxonomies($type) : [] as $tax){
293 if($terms = $tax == 'post_format' ? [] : get_the_terms($post, $tax)){
294 $type = array_merge((array)$type, get_taxonomy($tax)->object_type);
295 $tt_ids = array_merge($tt_ids, array_column($terms, 'term_taxonomy_id'));
296 }
297 }
298
299 if(!$tt_ids){
300 return false;
301 }
302
303 $vars += ['post_status'=>'publish', 'post__not_in'=>[$post->ID], 'post_type'=>array_unique($type), 'orderby'=>'related', 'term_taxonomy_ids'=>wpjam_filter($tt_ids, 'unique')];
304 }
305
306 $vars = $this->parse_vars($vars);
307
308 if($args){
309 $vars = array_filter(['posts_per_page'=>wpjam_pull($args, 'number')])+$vars;
310 $vars = wpjam_pull($args, ['post_type', 'orderby', 'posts_per_page'])+$vars;
311 $vars = ($days = wpjam_pull($args, 'days')) ? wpjam_set($vars, 'date_query[]', [
312 'column' => wpjam_pull($args, 'column') ?: 'post_date_gmt',
313 'after' => wpjam_date('Y-m-d', time()-DAY_IN_SECONDS*$days).' 00:00:00'
314 ]) : $vars;
315 }
316
317 return new WP_Query($vars+['no_found_rows'=>true, 'ignore_sticky_posts'=>true]);
318 }
319
320 public function parse($vars, $args=[]){
321 $format = wpjam_pull($args, 'format');
322 $parse = wpjam_pull($args, 'parse');
323
324 if(is_null($parse)){
325 $parse = true;
326 $args += $this->type == 'post' ? ['options_required'=>false, 'taxonomy_required'=>false] : [];
327 }
328
329 if($this->type == 'term'){
330 $object = ($tax = $vars['taxonomy'] ?? '') && is_string($tax) ? wpjam_get_taxonomy($tax) : null;
331 $terms = $vars['terms'] ?? null;
332 $depth = $args['depth'] ?? ($object ? $object->max_depth : null);
333
334 if($depth != -1 && $object && $object->hierarchical){
335 $nest = ['max_depth'=>($depth ?? (int)$object->levels), 'format'=>$format];
336 $nest += $parse ? ['item_callback'=>'wpjam_get_term'] : ['fields'=>['id'=>'term_id']];
337
338 if(($parent = (int)wpjam_pull($vars, 'parent')) && !($nest['top'] = get_term($parent))){
339 return [];
340 }
341
342 if($terms){
343 $ids = array_column($terms, 'term_id');
344 $terms = WPJAM_Term::get_by_ids(array_unique(array_reduce($ids, fn($c, $id)=> array_merge($c, get_ancestors($id, $tax, 'taxonomy')), $ids)));
345 }
346 }
347
348 return wpjam_then(
349 ($terms ?? get_terms($vars+['hide_empty'=>false])) ?: [],
350 fn($v)=> isset($nest) ? wpjam_nest($v, $nest) : ($parse ? array_values(array_map('wpjam_get_term', $v)) : $v)
351 );
352 }
353
354 $query = is_object($vars) ? $vars : $this->query($vars, $args);
355
356 if(!$query || !$parse){
357 return $query ? $query->posts : [];
358 }
359
360 $parsed = [];
361 $args += ['thumbnail_size'=>wpjam_pull($args, 'size')];
362 $args += $query->get('related_query') ? ['filter'=>'wpjam_related_post_json'] : [];
363
364 while($query->have_posts()){
365 $query->the_post();
366
367 if($item = wpjam_get_post(get_the_ID(), $args+['query'=>$query])){
368 $parsed = wpjam_set($parsed, ($format == 'date' ? wpjam_at($item['date'], ' ', 0) : '').'[]', $item);
369 }
370 }
371
372 if(!$query->is_main_query()){
373 wp_reset_postdata();
374 }
375
376 return $parsed;
377 }
378
379 public function parse_vars($vars, $param=false){
380 if(($type = $vars['post_type'] ?? '') && is_string($type) && str_contains($type, ',')){
381 $vars['post_type'] = wp_parse_list($type);
382 }
383
384 if($param === 'calendar'){
385 $args = ['year'=>['year', 'Y'], 'monthnum'=>['month', 'm'], 'day'=>['day', '']];
386 $vars += wpjam_map($args, fn($v)=> (int)wpjam_param($v[0]) ?: ($v[1] ? wpjam_date($v[1]) : null));
387 }elseif($param){
388 $number = wpjam_find(['number', 'posts_per_page'], fn($v)=> $v, fn($k)=> ($v = (int)wpjam_param($k)) > 0 ? $v : 0);
389 $ids = wpjam_param('post__in');
390 $vars += array_filter(['offset'=>wpjam_param('offset'), 'posts_per_page'=>min($number, 100)]);
391 $vars += $ids ? ['include'=>$ids, 'orderby'=>'post__in'] : [];
392 }
393
394 foreach(get_taxonomies([], 'objects') as $tax => $object){
395 if(in_array($tax, ['category', 'post_tag']) || !$object->_builtin){
396 $qk = wpjam_get_taxonomy_query_key($tax);
397 $keys = $tax == 'category' ? ['category_id', 'cat_id'] : [$qk];
398
399 if($value = ($param ? array_reduce($keys, fn($c, $k)=> (int)wpjam_param($k) ?: $c, 0) : 0) ?: wpjam_pull($vars, $qk)){
400 $term_ids[$tax] = $value;
401 }
402 }
403 }
404
405 if(!empty($vars['taxonomy']) && empty($vars['term']) && ($value = wpjam_pull($vars, 'term_id'))){
406 if(is_numeric($value)){
407 $term_ids[wpjam_pull($vars, 'taxonomy')] = $value;
408 }else{
409 $vars['term'] = $value;
410 }
411 }
412
413 foreach(array_filter($term_ids ?? []) as $tax => $value){
414 if($tax == 'category' && $value != 'none'){
415 $vars['cat'] = $value;
416 }else{
417 $vars['tax_query'][] = ['taxonomy'=>$tax, 'field'=>'term_id']+($value == 'none' ? ['operator'=>'NOT EXISTS'] : ['terms'=>[$value]]);
418 }
419 }
420
421 foreach(wpjam_pull($vars, ['include', 'exclude']) as $k => $v){
422 if($ids = wp_parse_id_list($v)){
423 $vars[$k == 'include' ? 'post__in' : 'post__not_in'] = $ids;
424
425 $k == 'include' && ($vars['posts_per_page'] = count($ids));
426 }
427 }
428
429 foreach(['cursor'=>'before', 'since'=>'after'] as $k => $v){
430 if($value = (int)(($param ? wpjam_param($k) : 0) ?: wpjam_pull($vars, $k))){
431 $vars['date_query'][] = [$v => wpjam_date('Y-m-d H:i:s', $value)];
432
433 $vars['ignore_sticky_posts'] = true;
434 }
435 }
436
437 return $vars;
438 }
439
440 public function render($vars, $args=[]){
441 $cb = wpjam_fill(['item_callback', 'wrap_callback'], fn($k)=> $args[$k] ?? [$this, $k]);
442 $query = is_object($vars) ? $vars : $this->query($vars, $args);
443 $args += ['query'=>$query];
444
445 return $query ? $cb['wrap_callback'](implode(wpjam_map($query->posts, fn($p, $i)=> $cb['item_callback']($p->ID, $args+['i'=>$i]))), $args) : '';
446 }
447
448 public function item_callback($post_id, $args){
449 $args += ['title_number'=>'', 'excerpt'=>false, 'thumb'=>true, 'size'=>'thumbnail', 'thumb_class'=>'wp-post-image', 'wrap_tag'=>'li'];
450 $title = get_the_title($post_id);
451 $item = wpjam_wrap($title);
452
453 $args['title_number'] && $item->before('span', ['title-number'], zeroise($args['i']+1, strlen(count($args['query']->posts))).'. ');
454 ($args['thumb'] || $args['excerpt']) && $item->wrap('h4');
455 $args['thumb'] && $item->before(get_the_post_thumbnail($post_id, $args['size'], ['class'=>$args['thumb_class']]));
456 $args['excerpt'] && $item->after(wpautop(get_the_excerpt($post_id)));
457
458 return $item->wrap('a', ['href'=>get_permalink($post_id), 'title'=>strip_tags($title)])->wrap($args['wrap_tag'])->render();
459 }
460
461 public function wrap_callback($output, $args){
462 if(!$output){
463 return '';
464 }
465
466 $args += ['title'=>'', 'div_id'=>'', 'class'=>[], 'thumb'=>true, 'wrap_tag'=>'ul'];
467 $output = wpjam_wrap($output);
468
469 $args['wrap_tag'] && $output->wrap($args['wrap_tag'])->add_class($args['class'])->add_class($args['thumb'] ? 'has-thumb' : '');
470 $args['title'] && $output->before($args['title'], 'h3');
471 $args['div_id'] && $output->wrap('div', ['id'=>$args['div_id']]);
472
473 return $output->render();
474 }
475
476 public static function get_instance($type){
477 static $objects = [];
478 return $objects[$type] ??= new self($type);
479 }
480 }
481
482 class WPJAM_Extend{
483 private $dir;
484 private $option;
485
486 public function __construct($dir, $args){
487 $this->dir = $dir;
488 $this->option = ($args['option'] ?? '') ? wpjam_register_option($args['option'], $args+[
489 'ajax' => false,
490 'site_default' => is_multisite() && ($args['sitewide'] ?? false),
491 'fields' => [$this, 'get_fields'],
492 'sanitize_callback' => [$this, 'sanitize']
493 ]) : null;
494 }
495
496 public function __call($method, $args){
497 $dir = $this->dir;
498 $option = $this->option;
499 $active = get_option('active_plugins') ?: [];
500 $data = $option && $method == 'load'
501 ? array_merge(array_filter($option->get_option()), $option->site_default ? array_filter($option->get_site_option()) : [])
502 : ($method == 'sanitize' ? $args[0] : array_filter(scandir($dir), fn($v)=> !in_array($v, ['.', '..', 'extends.php'])));
503
504 foreach($data as $k => $v){
505 $n = is_numeric($k) ? $v : $k;
506 $f = str_ends_with($n, '.php') ? $n : '';
507 $n = $f ? substr($n, 0, -4) : $n;
508 $e = $f ?: $n.(is_dir($dir.'/'.$n) ? '/'.$n : '').'.php';
509 $f = $n ? $dir.'/'.$e : '';
510 $d = is_file($f) ? wpjam_get_file_data($f) : '';
511
512 if($method == 'load'){
513 if($d && (is_admin() || empty($d['Admin'])) && !in_array($e, $active)){
514 include_once $f;
515 }
516 }elseif($method == 'get_fields'){
517 $v = $d && $d['Name'] && (!$option->site_default || is_network_admin() || !$option->get_site_setting($n)) ? [
518 'value' => [$option, 'get_'.(is_network_admin() ? 'site_': '').'setting']($n),
519 'title' => wpjam_wrap($d['Name'], $d['URI'] ? 'a' : '', ['href'=>$d['URI'], 'target'=>"_blank"]),
520 'label' => $d['Description']
521 ] : '';
522 }
523
524 if($d && $v){
525 $result[$n] = $v;
526 }
527 }
528
529 return $result ?? [];
530 }
531 }
532
533 class WPJAM_Param{
534 private $data = [];
535
536 public function __invoke($type, ...$args){
537 $name = $args && is_scalar($args[0]) ? array_shift($args) : null;
538 $args = $args[0] ?? [];
539 $value = $this->get_by($type, $name);
540
541 if(isset($name)){
542 $value ??= $args['default'] ?? wpjam_default($name);
543
544 if($name && ($field = wpjam_except($args, ['default', 'send']))){
545 $field = wpjam_field(['key'=>$name]+(($field['type'] ?? '') ? [] : ['_schema'=>[]])+$field);
546 $value = wpjam_catch([$field, 'validate'], $value, 'parameter');
547
548 ($args['send'] ?? true) && wpjam_if_error($value, 'send');
549 }
550 }else{
551 $fields = $args && is_array($args) ? wpjam_fields($args) : $args;
552 $value = $fields ? array_merge($value, $fields->validate($value, 'parameter')) : $value;
553 }
554
555 return $value;
556 }
557
558 private function get_by($type, $name=''){
559 $type = strtolower($type ?: 'get');
560
561 if($type == 'data'){
562 if($name && isset($_GET[$name])){
563 return wp_unslash($_GET[$name]);
564 }
565 }elseif($type != 'defaults'){
566 $data = ['post'=>$_POST, 'request'=>$_REQUEST][$type] ?? $_GET;
567
568 if($name){
569 if(isset($data[$name])){
570 return wp_unslash($data[$name]);
571 }
572
573 if($_POST || $type == 'get'){
574 return null;
575 }
576 }else{
577 if($data || $type != 'post'){
578 return wp_unslash($data);
579 }
580 }
581
582 $type = 'input';
583 }
584
585 $data = $this->data[$type] ??= $this->get_data($type);
586
587 return $name ? wpjam_get($data, $name) : $data;
588 }
589
590 private function get_data($type){
591 if($type == 'input'){
592 $v = file_get_contents('php://input');
593 $v = $v && is_string($v) ? @wpjam_json_decode($v) : $v;
594 $data = is_array($v) ? $v : [];
595 }else{
596 foreach(array_unique(['defaults', $type]) as $t){
597 $v = $this->get_by('request', $t) ?? [];
598 $v = $v && is_string($v) && str_starts_with($v, '{') ? wpjam_json_decode($v) : wp_parse_args($v ?: []);
599 $data = wpjam_merge($data ?? [], $v);
600 }
601 }
602
603 return $data;
604 }
605
606 public static function get_instance(){
607 static $object;
608 return $object ??= new self();
609 }
610 }
611
612 class WPJAM_Http{
613 public static function request($url, $args=[], $err=[]){
614 $throw = wpjam_pull($args, 'throw');
615 $field = wpjam_pull($args, 'field');
616
617 try{
618 $result = wpjam_try('wp_safe_remote_request', $url, self::prepare($args));
619 $result = self::parse($result, $args, $err);
620
621 return $field ? wpjam_get($result, $field) : $result;
622 }catch(Exception $e){
623 $error = wpjam_fill(['code', 'message', 'data'], fn($k)=> [$e, 'get_error_'.$k]());
624
625 if(apply_filters('wpjam_http_response_error_debug', true, $error['code'], $error['message'])){
626 trigger_error(var_export(array_filter(['url'=>$url, 'error'=>array_filter($error), 'body'=>$args['body'] ?? '']), true));
627 }
628
629 if($throw){
630 throw $e;
631 }
632
633 return wpjam_catch($e);
634 }
635 }
636
637 private static function prepare($args){
638 $args += ['body'=>[], 'sslverify'=>false];
639 $method = strtoupper($args['method'] ?? '') ?: ($args['body'] ? 'POST' : 'GET');
640
641 if($method == 'FILE'){
642 wpjam_once('pre_http_request', fn($pre, $args, $url)=> (new WP_Http_Curl())->request($url, $args), 1, 3);
643
644 $method = $args['body'] ? 'POST' : 'GET';
645 }elseif($method != 'GET'){
646 $key = 'content-type';
647 $type = 'application/json';
648
649 $args['headers'] = array_change_key_case($args['headers'] ?? []);
650
651 if(array_first(wpjam_pull($args, ['json_encode', 'need_json_encode']))){
652 $args['headers'][$key] = $type;
653 }
654
655 if(str_contains($args['headers'][$key] ?? '', $type) && is_array($args['body'])){
656 $args['body'] = wpjam_json_encode($args['body'] ?: new stdClass);
657 }
658 }
659
660 return ['method'=>$method]+$args;
661 }
662
663 private static function parse($result, $args, $err){
664 $code = $result['response']['code'];
665 $body = &$result['body'];
666
667 if(!wpjam_compare($code, 'between', [200, 299])){
668 wpjam_throw($code, $code.' - '.$result['response']['message'].'-'.var_export($body, true));
669 }
670
671 if($body && empty($args['stream'])){
672 if(str_contains(wp_remote_retrieve_header($result, 'content-disposition'), 'attachment;')){
673 $body = wpjam_bits($body);
674 }elseif(wpjam_pull($args, 'json_decode') !== false && str_starts_with($body, '{') && str_ends_with($body, '}')){
675 $body = wpjam_then(wpjam_json_decode($body), fn($res)=> self::error($res, $err+['errmsg'=>'errmsg']), fn()=> $body);
676 }
677 }
678
679 return $result;
680 }
681
682 private static function error($res, $err){
683 if(($code = wpjam_pull($res, $err['errcode'] ?? 'errcode')) && $code != ($err['success'] ?? '0')){
684 wpjam_throw($code, wpjam_pull($res, $err['errmsg']), wpjam_pull($res, $err['detail'] ?? 'detail') ?? array_filter($res));
685 }
686
687 if(strtolower($res[$err['errmsg']] ?? '') == 'ok'){
688 unset($res[$err['errmsg']]);
689 }
690
691 return $res;
692 }
693 }
694
695 class WPJAM_Error{
696 public static function parse($code='', $msg=''){
697 if(is_wp_error($code)){
698 $err = $code;
699 $code = $err->get_error_code();
700 $msg = $err->get_error_message();
701 $data = ['errcode'=>$code, 'errmsg'=>$msg]+array_filter(is_array($data = $err->get_error_data()) ? $data : ['errdata'=>$data]);
702 }elseif(is_array($code)){
703 $data = $code;
704 $code = $data['errcode'] ??= 0;
705 $msg = $data['errmsg'] ?? [];
706 }
707
708 if(isset($data)){
709 return self::can($code, $msg) ? array_merge($data, self::parse($code, $msg)) : $data;
710 }
711
712 $args = $msg ?: [];
713 $err = $code;
714
715 if($item = self::get($code)){
716 $msg = maybe_closure($item['errmsg'], $args);
717 }elseif(try_prefix($err, '-', 'invalid_')){
718 $err = $err == 'id' ? 'ID' : str_replace(['_id', '_'], [' ID', ' '], $err);
719 $msg = 'Invalid '.$err.($args ? ': %s.' : '.');
720
721 if(did_action('init')){
722 if(($trans = __($msg, 'wpjam')) != $msg){
723 $msg = $trans;
724 }else{
725 $msg = __('Invalid %s'.($args ? ': %s.': '.'), 'wpjam');
726 $args = [__($err, 'wpjam'), ...$args];
727 }
728 }
729 }elseif(try_suffix($err, '-', '_required')){
730 $msg = __('%s is empty or invalid.', 'wpjam');
731 $trans = __($args && $err == 'value' ? '%s\'s value' : $err, 'wpjam');
732 $args = [$args ? sprintf($trans.($err == 'value' ? '' : '%s'), ...$args) : $trans];
733 }elseif(try_suffix($err, '-', '_occupied')){
734 $msg = __('%s is already in use by another account.', 'wpjam');
735 $args = [__($err, 'wpjam')];
736 }else{
737 $msg = str_replace('_', ' ', $err);
738 }
739
740 if(!$msg){
741 return [];
742 }
743
744 return ['errcode'=>$code, 'errmsg'=>(($args && str_contains($msg, '%')) ? sprintf($msg, ...$args) : $msg)]+$item;
745 }
746
747 public static function can($code, $msg=''){
748 return $code && (!$msg || is_array($msg));
749 }
750
751 public static function callback($code, $msg, $data, $error){
752 if(self::can($code, $msg) && count($error->get_error_messages($code)) <= 1 && ($item = self::parse($code, $msg))){
753 $error->remove($code);
754 $error->add($code, $item['errmsg'], empty($item['modal']) ? $data : array_merge((is_array($data) ? $data : []), ['modal'=>$item['modal']]));
755 }
756 }
757
758 public static function __callStatic($method, $args){
759 static $object;
760 $object ??= wpjam_hook('wp_error_added', ['callback'=>[self::class, 'callback']], 10, 4);
761 $key = 'errors['.(array_shift($args) ?: '').']';
762
763 return $method == 'get' ? ($object->get_arg($key) ?: []) : $object->update_arg($key, ['errmsg'=>$args[0], 'modal'=>$args[1] ?? []]);
764 }
765 }
766
767 class WPJAM_Hook extends WPJAM_Args{
768 public function __invoke(...$args){
769 if($this->check && !($this->check)(...$args)){ // del 2026-08-30
770 trigger_error(var_export($this->check, true));
771 return $args[0];
772 }
773
774 $value = $args[0] ?? null;
775 $hook = $this->hook = current_filter();
776 $args = ($this->op || $this->maybe || $this->tap) ? wpjam_rotate($args, 1) : $args;
777 $result = $this->bind ? $this->call('callback', ...$args) : wpjam_call($this->callback, ...$args);
778 $result = $this->op ? wpjam_operate($value, $this->op, $result) : $result;
779
780 if($this->once && !($this->maybe && (did_action($hook) ? $result === false : is_null($result)))){
781 if($filter = $GLOBALS['wp_filter'][$hook] ?? null){
782 unset($filter->callbacks[$filter->current_priority()][wpjam_callback('uniqid', $this)]);
783 }
784 }
785
786 return $this->echo ? wpjam_echo($result) : ($this->tap ? $value : ($this->maybe ? ($result ?? $value) : $result));
787 }
788
789 public static function batch($action, $name, ...$args){
790 if(is_array($name) || str_contains($name ?: '', ',')){
791 return wpjam_call(
792 $action === 'remove' ? 'wpjam_filter' : 'wpjam_map',
793 array_filter(is_array($name) ? $name : wp_parse_list($name)),
794 fn($n)=> self::batch($action, ...(is_array($n) ? $n : [$n, ...$args]))
795 );
796 }
797
798 if($name && $args){
799 $cb = array_shift($args);
800 $multi = wp_is_numeric_array($cb) && !is_callable($cb);
801 $result = wpjam_map($multi ? $cb : [$cb], fn($cb)=> self::$action($name, $cb, ...$args));
802
803 return $multi ? $result : $result[0];
804 }
805 }
806
807 public static function add($name, $cb, ...$args){
808 if(wpjam_is_assoc_array($cb)){
809 $cb = new static($cb);
810 }else{
811 if($attr = $name === 'once' ? [$name=>true] : []){
812 $name = $cb;
813 $cb = array_shift($args);
814 }
815
816 if($type = in_array($cb, ['+', '-', '.'], true) ? 'op' : (in_array($cb, ['tap', 'maybe', 'echo', 'bind'], true) ? $cb : '')){
817 $attr += [$type=>$cb];
818 $cb = array_shift($args);
819 }
820
821 $cb = is_callable($cb) ? $cb : fn()=> $cb;
822 $cb = $attr ? new static($attr+['callback'=>$cb]) : $cb;
823 }
824
825 add_filter($name, $cb, ...$args);
826
827 return $cb;
828 }
829
830 public static function remove(...$args){
831 return count($args) > 1 && ($args[2] ??= has_filter(...$args)) !== false && remove_filter(...$args);
832 }
833 }
834
835 class WPJAM_AJAX extends WPJAM_Args{
836 public function callback(){
837 add_filter('wp_die_ajax_handler', fn()=> 'wpjam_die_handler');
838
839 $data = wpjam_params('post');
840
841 if(!$this->admin){
842 $data = array_merge(wpjam_params('data'), wpjam_except($data, ['action', 'defaults', 'data', '_ajax_nonce']));
843 }
844
845 if($fields = $this->fields){
846 $data = array_merge($data, wpjam_fields($fields)->validate($data, 'parameter'));
847 }
848
849 if(($this->verify ?? true) && ($action = $this->nonce_action($data)) && !check_ajax_referer($action, false, false)){
850 wp_die('验证失败,请刷新重试。', 'invalid_nonce');
851 }
852
853 if(($allow = $this->allow) && !wpjam_call($allow, $data)){
854 wp_die('access_denied');
855 }
856
857 return wpjam_send_json(wpjam_catch($this->callback, $data, $this->name));
858 }
859
860 public function nonce_action($args){
861 $cb = $this->nonce_action;
862
863 return $cb ? $cb($args) : ($this->admin ? '' : $this->name.wpjam_join(':', wpjam_pick($args, $this->nonce_keys ?: [])));
864 }
865
866 public function registered(){
867 if(wp_doing_ajax() && wpjam_param('action', 'request') == $this->name){
868 $part = is_user_logged_in() ? '' : ($this->nopriv ? 'nopriv_' : false);
869
870 $part !== false && add_action('wp_ajax_'.$part.$this->name, [$this, 'callback']);
871 }
872 }
873
874 public static function register($name, $args=[]){
875 return wpjam_register(static::class, $name, $args);
876 }
877
878 public static function get($name){
879 return wpjam_get_registered(static::class, $name);
880 }
881
882 public static function add_hooks(){
883 wpjam_script('wpjam-ajax', [
884 'for' => 'wp,login',
885 'src' => wpjam_url(dirname(__DIR__).'/static/ajax.js'),
886 'deps' => ['jquery'],
887 'data' => 'var ajaxurl = "'.admin_url('admin-ajax.php').'";',
888 'position' => 'before',
889 'priority' => 1
890 ]);
891 }
892 }
893
894 class WPJAM_File extends WPJAM_Args{
895 public function __invoke($value, $to, $from=null){
896 $from ??= is_numeric($value) ? 'id' : 'file';
897
898 return $from == $to ? $value : ($from == 'id' ? $this->from_id($value, $to) : $this->convert($value, $to, $from));
899 }
900
901 protected function convert($value, $to, $from){
902 if(in_array($to, ['size', 'ext'])){
903 $file = $from == 'file' ? $value : $this->convert($value, 'file', $from);
904
905 if($to == 'ext'){
906 return pathinfo($file, PATHINFO_EXTENSION);
907 }
908
909 return file_exists($file) && ($size = wp_getimagesize($file))
910 ? ['width'=>$size[0], 'height'=>$size[1]]
911 : $this->from_id($this->convert($file, 'id', 'file'), 'size');
912 }
913
914 if($path = $this->to_path($value, $from)){
915 return $to == 'id' ? wpjam_get_by_meta('post', '_wp_attached_file', ltrim($path, '/'), 'object_id') : (['url'=>$this->baseurl,'file'=>$this->basedir][$to] ?? '').$path;
916 }
917 }
918
919 protected function to_path($value, $from='file'){
920 if($from == 'file'){
921 $base = $this->basedir;
922 }elseif($from == 'url'){
923 $value = parse_url($value, PHP_URL_PATH);
924 $base = parse_url($this->baseurl, PHP_URL_PATH);
925 }else{
926 return $value;
927 }
928
929 return try_prefix($value, '-', $base) ? $value : null;
930 }
931
932 protected function from_id($id, $to='file'){
933 if($to == 'url'){
934 return wp_get_attachment_url($id);
935 }
936
937 if($id && get_post_type($id) == 'attachment'){
938 if(in_array($to, ['meta', 'size'])){
939 $meta = wp_get_attachment_metadata($id);
940 $meta = is_array($meta) ? $meta : [];
941
942 return $to == 'meta' ? $meta : wpjam_pick($meta, ['width', 'height']);
943 }
944
945 $file = get_attached_file($id, true);
946
947 return $to == 'file' ? $file : $this->convert($file, $to, 'file');
948 }
949 }
950
951 public function restore($id, $url=''){
952 if(is_dir(dirname($file))){
953 mkdir(dirname($file), 0777, true);
954 }
955
956 wpjam_remote_request($url ?: $this->from_id($id, 'url'), ['throw'=>true, 'stream'=>true, 'filename'=>$file]);
957
958 return $file;
959 }
960
961 public function attach($file, $args=[]){
962 require_once ABSPATH.'wp-admin/includes/image.php';
963
964 $meta = wp_read_image_metadata($file);
965 $title = $meta ? trim($meta['title']) : '';
966 $title = ($title && !is_numeric(sanitize_title($title))) ? $title : preg_replace('/\.[^.]+$/', '', wp_basename($file));
967 $pid = $args['post_id'] ?? 0;
968 $id = wpjam_try('wp_insert_attachment', [
969 'post_title' => $title,
970 'post_content' => $meta ? (trim($meta['caption']) ?: '') : '',
971 'post_parent' => $pid,
972 'post_mime_type' => $args['type'] ?? mime_content_type($file),
973 'guid' => $args['url'] ?? $this->convert($file, 'url', 'file'),
974 ], $file, $pid, true);
975
976 wp_update_attachment_metadata($id, wp_generate_attachment_metadata($id, $file));
977
978 return $id;
979 }
980
981 public function mimes($accept){
982 $allowed = get_allowed_mime_types();
983 $types = [];
984
985 foreach(wpjam_lines($accept ?: '', ',', fn($v)=> strtolower($v)) as $v){
986 if(str_ends_with($v, '/*')){
987 $prefix = substr($v, 0, -1);
988 $types += array_filter($allowed, fn($m)=> str_starts_with($m, $prefix));
989 }elseif(str_contains($v, '/')){
990 $ext = array_search($v, $allowed);
991 $types += $ext ? [$ext => $v] : [];
992 }elseif(($v = ltrim($v, '.')) && preg_match('/^[a-z0-9]+$/', $v)){
993 $ext = array_find_key($allowed, fn($m, $ext)=> str_contains($ext, '|') ? in_array($v, explode('|', $ext)) : $v == $ext);
994 $types += $ext ? wpjam_pick($allowed, [$ext]) : [];
995 }
996 }
997
998 return $types;
999 }
1000
1001 public function upload($name, $args=[]){
1002 require_once ABSPATH.'wp-admin/includes/file.php';
1003
1004 if(!empty($args['accept'])){
1005 $args['mimes'] = $this->mimes($args['accept']) ?: wpjam_throw('upload_error', '无效的文件类型');
1006 }
1007
1008 if($bits = wpjam_pull($args, 'bits')){
1009 $mime = preg_match('/data:([^;]+);base64,/i', $bits, $m) ? $m[1] : wpjam_throw('upload_error', '无效的 data URI 格式');
1010 $ext = (empty($args['mimes']) || in_array($mime, $args['mimes'])) ? array_search($mime, get_allowed_mime_types()) : '';
1011 $bits = $ext ? base64_decode(trim(substr($bits, strlen($m[0])))) : wpjam_throw('upload_error', '不�
1012 �许的文件类型');
1013 $upload = wp_upload_bits(explode_last('.', $name)[0].'.'.(explode('|', $ext)[0]), null, $bits);
1014 }else{
1015 if(is_array($name)){
1016 $action = 'sideload';
1017 }else{
1018 $action = 'upload';
1019 $name = $_FILES[$name] ?? wpjam_throw('invalid_parameter', [$name]);
1020 }
1021
1022 if(wpjam_pull($args, 'media')){
1023 require_once ABSPATH.'wp-admin/includes/media.php';
1024 require_once ABSPATH.'wp-admin/includes/image.php';
1025
1026 return wpjam_try('media_handle_'.$action, $name, ($args['post_id'] ?? 0));
1027 }
1028
1029 $upload = wpjam_call('wp_handle_'.$action, $name, $args+['test_form'=>false]);
1030 }
1031
1032 return empty($upload['error']) ? $upload+['path'=>$this->to_path($upload['file'])] : wpjam_throw('upload_error', $upload['error']);
1033 }
1034
1035 public function download($url, $args=[]){
1036 $media = $args['media'] ?? false;
1037 $field = ($args['field'] ?? '') ?: ($media ? 'id' : 'file');
1038 $result = wpjam_get_by_meta('post', 'source_url', $url, 'object_id');
1039
1040 if(!$result || get_post_type($result) != 'attachment'){
1041 try{
1042 $tmp = wpjam_try('download_url', $url);
1043 $upload = ['name'=>($args['name'] ?? '') ?: md5($url).'.'.wpjam_at(wp_get_image_mime($tmp), '/', 1), 'tmp_name'=>$tmp];
1044 $result = $this->upload($upload, wpjam_pick($args, ['media', 'post_id']));
1045
1046 if(!$media){
1047 return $result[$field];
1048 }
1049
1050 update_post_meta($result, 'source_url', $url);
1051 }catch(Exception $e){
1052 $tmp && @unlink($tmp);
1053
1054 return wpjam_catch($e);
1055 }
1056 }
1057
1058 return $this->from_id($result, $field);
1059 }
1060
1061 public function parse($file, $type='data'){
1062 $data = $file ? array_reduce(['URI', 'Name'], fn($c, $k)=> wpjam_set($c, $k, ($c[$k] ?? '') ?: ($c['Plugin'.$k] ?? '')), get_file_data($file, [
1063 'Admin' => 'Admin',
1064 'Name' => 'Name',
1065 'URI' => 'URI',
1066 'PluginName' => 'Plugin Name',
1067 'PluginURI' => 'Plugin URI',
1068 'Version' => 'Version',
1069 'Description' => 'Description'
1070 ])) : [];
1071
1072 return $type == 'summary' ? ($data ? str_replace('', '', $data['Description']).'详细介绍请点击:<a href="'.$data['URI'].'" target="_blank">'.$data['Name'].'</a>。' : '') : $data;
1073 }
1074
1075 public static function get_instance(){
1076 static $object;
1077 return $object ??= new self(wp_get_upload_dir());
1078 }
1079 }
1080
1081 class WPJAM_Qrcode extends WPJAM_Args{
1082 public function cache($action, ...$args){
1083 return [$this->handler, 'cache_'.$action](...$args);
1084 }
1085
1086 public function verify($scene, $code){
1087 $qrcode = $this->cache('get', $scene.'_scene');
1088
1089 if(!$qrcode || !$code || empty($qrcode['openid']) || $code != $qrcode['code']){
1090 return new WP_Error('invalid_'.($qrcode ? 'code' : 'qrcode'));
1091 }
1092
1093 $this->cache('delete', $scene.'_scene');
1094
1095 return $qrcode;
1096 }
1097
1098 public function scan($scene, $openid){
1099 $qrcode = $this->cache('get', $scene.'_scene');
1100
1101 if(!$qrcode || (!empty($qrcode['openid']) && $qrcode['openid'] != $openid)){
1102 return new WP_Error('invalid_qrcode');
1103 }
1104
1105 $this->cache('delete', $qrcode['key'].'_qrcode');
1106
1107 if(!empty($qrcode['id']) && ($cb = $qrcode['bind_callback'] ?? '') && is_callable($cb)){
1108 return $cb($openid, $qrcode['id']);
1109 }
1110
1111 $this->cache('set', $scene.'_scene', ['openid'=>$openid]+$qrcode, 1200);
1112
1113 return $qrcode['code'];
1114 }
1115
1116 public function create($key, $args=[]){
1117 return $this->cache('get', $key.'_qrcode') ?: wpjam_tap(wpjam_call($this->creator, $key, $args), fn($v)=> wpjam_map([
1118 $key.'_qrcode',
1119 $v['scene'].'_scene'
1120 ], fn($k)=> $this->cache('set', $k, $v, 1200)));
1121 }
1122
1123 public function get_fields($action='login'){
1124 if($action == 'bind'){
1125 $args = ['id'=>get_current_user_id()];
1126 $args = [md5('bind_'.$args['id']), $args];
1127 }else{
1128 $args = [wp_generate_password(32, false, false)];
1129 }
1130
1131 return wpjam_then($this->create(...$args), fn($v)=> [
1132 'qrcode' => ['type'=>'view', 'title'=>$this->title, 'value'=>'<img src="'.(($v['qrcode_url'] ?? '') ?: ($v[ 'qrcode'] ?? '')).'" width="272" />'],
1133 'code' => ['type'=>'number', 'title'=>'验证码', 'class'=>'input', 'required', 'size'=>20],
1134 'scene' => ['type'=>'hidden', 'value'=>$v['scene']]
1135 ]);
1136 }
1137 }
1138
1139 class WPJAM_Widget extends WP_Widget{
1140 public function __construct($id, $name, $options){
1141 parent::__construct($id, $name, $options+['show_instance_in_rest'=>false]);
1142 }
1143
1144 public function widget($args, $instance){
1145 $widget = $this->widget_options['widget'];
1146
1147 if($output = $widget($instance)){
1148 $title = wpjam_pull($instance, 'title');
1149 $output = ($title ? $args['before_title'].$title.$args['after_title'] : '').$output;
1150
1151 echo $args['before_widget'].$output.$args['after_widget'];
1152 }
1153 }
1154
1155 public function form($instance){
1156 echo str_replace('fieldset', 'span', wpjam_fields(wpjam_map(maybe_callback($this->widget_options['fields']), fn($v, $k)=> $v+[
1157 'id' => $this->get_field_id($k),
1158 'name' => $this->get_field_name($k),
1159 'value' => $instance[$k] ?? null
1160 ]))->render(['wrap_tag'=>'p']));
1161 }
1162 }
1163
1164 class WPJAM_Exception extends Exception{
1165 private $error;
1166
1167 public function __construct($msg, $code=null, ?Throwable $previous=null){
1168 $error = $this->error = is_wp_error($msg) ? $msg : new WP_Error($code ?: 'error', $msg);
1169 $code = $error->get_error_code();
1170
1171 parent::__construct($error->get_error_message(), (is_numeric($code) ? (int)$code : 1), $previous);
1172 }
1173
1174 public function __call($method, $args){
1175 return in_array($method, ['get_wp_error', 'get_error']) ? $this->error : [$this->error, $method](...$args);
1176 }
1177 }