class="language-py">+ for vehicle in self.vehicles: 180
+            i = self.update_vehicle(vehicle, matches)
181
+            if i is not None:
182
+                del matches[i]
183
+
184
+        # Add new vehicles based on the remaining matches
185
+        for match in matches:
186
+            contour, centroid = match
187
+            new_vehicle = Vehicle(self.next_vehicle_id, centroid)
188
+            self.next_vehicle_id += 1
189
+            self.vehicles.append(new_vehicle)
190
+            self.log.debug("Created new vehicle #%d from match (%d, %d)."
191
+                , new_vehicle.id, centroid[0], centroid[1])
192
+
193
+        # Count any uncounted vehicles that are past the divider
194
+        for vehicle in self.vehicles:
195
+            if not vehicle.counted and (((vehicle.last_position[1] > self.divider) and (vehicle.vehicle_dir == 1)) or
196
+                                          ((vehicle.last_position[1] < self.divider) and (vehicle.vehicle_dir == -1))) and (vehicle.frames_seen > 6):
197
+
198
+                vehicle.counted = True
199
+                # update appropriate counter
200
+                if ((vehicle.last_position[1] > self.divider) and (vehicle.vehicle_dir == 1) and (vehicle.last_position[0] >= (int(frame_w/2)-10))):
201
+                    self.vehicle_RHS += 1
202
+                    self.vehicle_count += 1
203
+                elif ((vehicle.last_position[1] < self.divider) and (vehicle.vehicle_dir == -1) and (vehicle.last_position[0] <= (int(frame_w/2)+10))):
204
+                    self.vehicle_LHS += 1
205
+                    self.vehicle_count += 1
206
+
207
+                self.log.debug("Counted vehicle #%d (total count=%d)."
208
+                    , vehicle.id, self.vehicle_count)
209
+
210
+        # Optionally draw the vehicles on an image
211
+        if output_image is not None:
212
+            for vehicle in self.vehicles:
213
+                vehicle.draw(output_image)
214
+
215
+            # LHS
216
+            cv2.putText(output_image, ("LH Lane: %02d" % self.vehicle_LHS), (12, 56)
217
+                , cv2.FONT_HERSHEY_PLAIN, 1.2, (127,255, 255), 2)
218
+            # RHS
219
+            cv2.putText(output_image, ("RH Lane: %02d" % self.vehicle_RHS), (216, 56)
220
+                , cv2.FONT_HERSHEY_PLAIN, 1.2, (127, 255, 255), 2)
221
+
222
+        # Remove vehicles that have not been seen long enough
223
+        removed = [ v.id for v in self.vehicles
224
+            if v.frames_since_seen >= self.max_unseen_frames ]
225
+        self.vehicles[:] = [ v for v in self.vehicles
226
+            if not v.frames_since_seen >= self.max_unseen_frames ]
227
+        for id in removed:
228
+            self.log.debug("Removed vehicle #%d.", id)
229
+
230
+        self.log.debug("Count updated, tracking %d vehicles.", len(self.vehicles))
231
+
232
+# ============================================================================
233
+
234
+def process_video():
235
+    global frame_no
236
+    global frame_w
237
+    camera = re.match(r".*/(\d+)_.*", inputFile)
238
+    camera = camera.group(1)
239
+
240
+# import video file
241
+    cap = cv2.VideoCapture(inputFile)
242
+
243
+# get list of background files
244
+    f = []
245
+    for (_, _, filenames) in walk(loc+"/backgrounds/"):
246
+        f.extend(filenames)
247
+        break
248
+
249
+# if background exists for camera: import, else avg will be built on fly
250
+    if camera+"_bg.jpg" in f:
251
+        bg = loc+"/backgrounds/"+camera+"_bg.jpg"
252
+        default_bg = cv2.imread(bg)
253
+        default_bg = cv2.cvtColor(default_bg, cv2.COLOR_BGR2HSV)
254
+        (_,avgSat,default_bg) = cv2.split(default_bg)
255
+        avg = default_bg.copy().astype("float")
256
+    else:
257
+        avg = None
258
+
259
+# get frame size
260
+    frame_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
261
+    frame_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
262
+
263
+# create a mask (manual for each camera)
264
+    mask = np.zeros((frame_h,frame_w), np.uint8)
265
+    mask[:,:] = 255
266
+    mask[:100, :] = 0
267
+    mask[230:, 160:190] = 0
268
+    mask[170:230,170:190] = 0
269
+    mask[140:170,176:190] = 0
270
+    mask[100:140,176:182] = 0
271
+
272
+# The cutoff for threshold. A lower number means smaller changes between
273
+# the average and current scene are more readily detected.
274
+    THRESHOLD_SENSITIVITY = 40
275
+    t_retval.append(THRESHOLD_SENSITIVITY)
276
+# Blob size limit before we consider it for tracking.
277
+    CONTOUR_WIDTH = 21
278
+    CONTOUR_HEIGHT = 16#21
279
+# The weighting to apply to "this" frame when averaging. A higher number
280
+# here means that the average scene will pick up changes more readily,
281
+# thus making the difference between average and current scenes smaller.
282
+    DEFAULT_AVERAGE_WEIGHT = 0.01
283
+    INITIAL_AVERAGE_WEIGHT = DEFAULT_AVERAGE_WEIGHT / 50
284
+# Blob smoothing function, to join 'gaps' in cars
285
+    SMOOTH = max(2,int(round((CONTOUR_WIDTH**0.5)/2,0)))
286
+# Constants for drawing on the frame.
287
+    LINE_THICKNESS = 1
288
+
289
+    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
290
+    out = loc+'/outputs/'+camera+'_output.mp4'
291
+#print(out)
292
+#exit()
293
+    out = cv2.VideoWriter(out, fourcc, 20, (frame_w, frame_h))
294
+
295
+    outblob = loc+'/outputs/'+camera+'_outblob.mp4'
296
+    diffop = loc+'/outputs/'+camera+'_outdiff.mp4'
297
+    outblob = cv2.VideoWriter(outblob, fourcc, 20, (frame_w, frame_h))
298
+    diffop = cv2.VideoWriter(diffop, fourcc, 20, (frame_w, frame_h))
299
+
300
+# A list of "tracked blobs".
301
+    blobs = []
302
+    car_counter = None  # will be created later
303
+    frame_no = 0
304
+
305
+    total_frames = cap.get(cv2.CAP_PROP_FRAME_COUNT)
306
+    total_cars = 0
307
+
308
+    start_time = time.time()
309
+    ret, frame = cap.read()
310
+
311
+    while ret:
312
+        ret, frame = cap.read()
313
+        frame_no = frame_no + 1
314
+
315
+        if ret and frame_no < total_frames:
316
+
317
+            print("Processing frame ",frame_no)
318
+
319
+            # get returned time
320
+            frame_time = time.time()
321
+
322
+            # convert BGR to HSV
323
+            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
324
+
325
+            # only use the Value channel of the frame
326
+            (_,_,grayFrame) = cv2.split(frame)
327
+            grayFrame = cv2.bilateralFilter(grayFrame, 11, 21, 21)
328
+
329
+            if avg is None:
330
+                # Set up the average if this is the first time through.
331
+                avg = grayFrame.copy().astype("float")
332
+                continue
333
+
334
+            # Build the average scene image by accumulating this frame
335
+            # with the existing average.
336
+            if frame_no < 10:
337
+                def_wt = INITIAL_AVERAGE_WEIGHT
338
+            else:
339
+                def_wt = DEFAULT_AVERAGE_WEIGHT
340
+
341
+            cv2.accumulateWeighted(grayFrame, avg, def_wt)
342
+
343
+            # export averaged background for use in next video feed run
344
+            #if frame_no > int(total_frames * 0.975):
345
+            if frame_no > int(200):
346
+                grayOp = cv2.cvtColor(cv2.convertScaleAbs(avg), cv2.COLOR_GRAY2BGR)
347
+                backOut = loc+"/backgrounds/"+camera+"_bg.jpg"
348
+                cv2.imwrite(backOut, grayOp)
349
+
350
+            # Compute the grayscale difference between the current grayscale frame and
351
+            # the average of the scene.
352
+            differenceFrame = cv2.absdiff(grayFrame, cv2.convertScaleAbs(avg))
353
+            # blur the difference image
354
+            differenceFrame = cv2.GaussianBlur(differenceFrame, (5, 5), 0)
355
+#        cv2.imshow("difference", differenceFrame)
356
+            diffout = cv2.cvtColor(differenceFrame, cv2.COLOR_GRAY2BGR)
357
+            diffop.write(diffout)
358
+
359
+            # get estimated otsu threshold level
360
+            retval, _ = cv2.threshold(differenceFrame, 0, 255,
361
+                                      cv2.THRESH_BINARY+cv2.THRESH_OTSU)
362
+            # add to list of threshold levels
363
+            t_retval.append(retval)
364
+
365
+            # apply threshold based on average threshold value
366
+            if frame_no < 10:
367
+                ret2, thresholdImage = cv2.threshold(differenceFrame,
368
+                                                     int(np.mean(t_retval)*0.9),
369
+                                                     255, cv2.THRESH_BINARY)
370
+            else:
371
+                ret2, thresholdImage = cv2.threshold(differenceFrame,
372
+                                                 int(np.mean(t_retval[-10:-1])*0.9),
373
+                                                 255, cv2.THRESH_BINARY)
374
+
375
+            # We'll need to fill in the gaps to make a complete vehicle as windows
376
+            # and other features can split them!
377
+            kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (SMOOTH, SMOOTH))
378
+            # Fill any small holes
379
+            thresholdImage = cv2.morphologyEx(thresholdImage, cv2.MORPH_CLOSE, kernel)
380
+
381
+            # Remove noise
382
+            thresholdImage = cv2.morphologyEx(thresholdImage, cv2.MORPH_OPEN, kernel)
383
+
384
+            # Dilate to merge adjacent blobs
385
+            thresholdImage = cv2.dilate(thresholdImage, kernel, iterations = 2)
386
+
387
+            # apply mask
388
+            thresholdImage = cv2.bitwise_and(thresholdImage, thresholdImage, mask = mask)
389
+#        cv2.imshow("threshold", thresholdImage)
390
+            threshout = cv2.cvtColor(thresholdImage, cv2.COLOR_GRAY2BGR)
391
+            outblob.write(threshout)
392
+
393
+            # Find contours aka blobs in the threshold image.
394
+            contours, hierarchy = cv2.findContours(thresholdImage,
395
+                                                      cv2.RETR_EXTERNAL,
396
+                                                      cv2.CHAIN_APPROX_SIMPLE)
397
+
398
+            print("Found ",len(contours)," vehicle contours.")
399
+            # process contours if they exist!
400
+            if contours:
401
+                for (i, contour) in enumerate(contours):
402
+                    # Find the bounding rectangle and center for each blob
403
+                    (x, y, w, h) = cv2.boundingRect(contour)
404
+                    contour_valid = (w > CONTOUR_WIDTH) and (h > CONTOUR_HEIGHT)
405
+
406
+                    print("Contour #",i,": pos=(x=",x,", y=",y,") size=(w=",w,
407
+                          ", h=",h,") valid=",contour_valid)
408
+
409
+                    if not contour_valid:
410
+                        continue
411
+
412
+                    center = (int(x + w/2), int(y + h/2))
413
+                    blobs.append(((x, y, w, h), center))
414
+
415
+            for (i, match) in enumerate(blobs):
416
+                contour, centroid = match
417
+                x, y, w, h = contour
418
+
419
+                # store the contour data
420
+                c = dict(
421
+                            frame_no = frame_no,
422
+                            centre_x = x,
423
+                            centre_y = y,
424
+                            width = w,
425
+                            height = h
426
+                            )
427
+                tracked_conts.append(c)
428
+
429
+                cv2.rectangle(frame, (x, y), (x + w - 1, y + h - 1), (0, 0, 255), LINE_THICKNESS)
430
+                cv2.circle(frame, centroid, 2, (0, 0, 255), -1)
431
+
432
+            if car_counter is None:
433
+                print("Creating vehicle counter...")
434
+                car_counter = VehicleCounter(frame.shape[:2], 2*frame.shape[0] / 3)
435
+
436
+            # get latest count
437
+            car_counter.update_count(blobs, frame)
438
+            current_count = car_counter.vehicle_RHS + car_counter.vehicle_LHS
439
+
440
+            # print elapsed time to console
441
+            elapsed_time = time.time()-start_time
442
+            print("-- %s seconds --" % round(elapsed_time,2))
443
+
444
+            # output video
445
+            frame = cv2.cvtColor(frame, cv2.COLOR_HSV2BGR)
446
+
447
+            # draw dividing line
448
+            # flash green when new car counted
449
+            if current_count > total_cars:
450
+                cv2.line(frame, (0, int(2*frame_h/3)),(frame_w, int(2*frame_h/3)),
451
+                     (0,255,0), 2*LINE_THICKNESS)
452
+            else:
453
+                cv2.line(frame, (0, int(2*frame_h/3)),(frame_w, int(2*frame_h/3)),
454
+                 (0,0,255), LINE_THICKNESS)
455
+
456
+             # update with latest count
457
+            total_cars = current_count
458
+
459
+            # draw upper limit
460
+            cv2.line(frame, (0, 100),(frame_w, 100), (0,0,0), LINE_THICKNESS)
461
+
462
+            ret, buffer = cv2.imencode('.jpg', frame)
463
+            frame2 = buffer.tobytes()
464
+            yield (b'--frame\r\n'
465
+                b'Content-Type: image/jpeg\r\n\r\n' + frame2 + b'\r\n')  # concat frame one by one and show result
466
+
467
+            #cv2.imshow("preview", frame)
468
+            #cv2.imwrite("../flask-hls-demo/static/frame.jpg", frame)
469
+            out.write(frame)
470
+
471
+            if cv2.waitKey(27) and 0xFF == ord('q'):
472
+                break
473
+        else:
474
+            break
475
+
476
+    #cv2.line()
477
+    #cv2.destroyAllWindows()
478
+    #cap.release()
479
+    #out.release()
480
+
481
+from flask import Flask, render_template, Response
482
+import cv2
483
+
484
+app = Flask(__name__)
485
+
486
+
487
+def find_camera(id):
488
+    '''
489
+    cameras = ['rtsp://username:password@ip_address:554/user=username_password='password'_channel=channel_number_stream=0.sdp',
490
+    'rtsp://username:password@ip_address:554/user=username_password='password'_channel=channel_number_stream=0.sdp']
491
+    '''
492
+    cameras = ['rtsp://admin:@Unv123456@192.168.10.252:554/unicast/c1/s1/live']
493
+    return cameras[int(id)]
494
+#  for cctv camera use rtsp://username:password@ip_address:554/user=username_password='password'_channel=channel_number_stream=0.sdp' instead of camera
495
+#  for webcam use zero(0)
496
+
497
+
498
+def gen_frames(camera_id):
499
+
500
+    cam = find_camera(camera_id)
501
+    cap=  cv2.VideoCapture(cam)
502
+
503
+    while True:
504
+        # for cap in caps:
505
+        # # Capture frame-by-frame
506
+        success, frame = cap.read()  # read the camera frame
507
+        if not success:
508
+            break
509
+        else:
510
+            ret, buffer = cv2.imencode('.jpg', frame)
511
+            frame = buffer.tobytes()
512
+            yield (b'--frame\r\n'
513
+                b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')  # concat frame one by one and show result
514
+@app.route('/video_feed/<string:id>/', methods=["GET"])
515
+def video_feed(id):
516
+
517
+    """Video streaming route. Put this in the src attribute of an img tag."""
518
+    '''
519
+    return Response(gen_frames(id),
520
+                    mimetype='multipart/x-mixed-replace; boundary=frame')
521
+    '''
522
+    return Response(process_video(),
523
+                    mimetype='multipart/x-mixed-replace; boundary=frame')
524
+
525
+@app.route('/', methods=["GET"])
526
+def index():
527
+    return render_template('index.html')
528
+
529
+
530
+if __name__ == '__main__':
531
+    app.run(debug=True, port=9099)

BIN
cap/frame0.jpg


BIN
cap/frame1.jpg


BIN
cap/frame10.jpg


BIN
cap/frame100.jpg


BIN
cap/frame101.jpg


BIN
cap/frame102.jpg


BIN
cap/frame103.jpg


BIN
cap/frame104.jpg


BIN
cap/frame105.jpg


BIN
cap/frame106.jpg


BIN
cap/frame107.jpg


BIN
cap/frame108.jpg


BIN
cap/frame109.jpg


BIN
cap/frame11.jpg


BIN
cap/frame110.jpg


BIN
cap/frame111.jpg


BIN
cap/frame112.jpg


BIN
cap/frame113.jpg


BIN
cap/frame114.jpg


BIN
cap/frame115.jpg


BIN
cap/frame116.jpg


BIN
cap/frame117.jpg


BIN
cap/frame118.jpg


BIN
cap/frame119.jpg


BIN
cap/frame12.jpg


BIN
cap/frame120.jpg


BIN
cap/frame121.jpg


BIN
cap/frame122.jpg


BIN
cap/frame123.jpg


BIN
cap/frame124.jpg


BIN
cap/frame125.jpg


BIN
cap/frame126.jpg


BIN
cap/frame127.jpg


BIN
cap/frame128.jpg


BIN
cap/frame129.jpg


BIN
cap/frame13.jpg


BIN
cap/frame130.jpg


BIN
cap/frame131.jpg


BIN
cap/frame132.jpg


BIN
cap/frame133.jpg


BIN
cap/frame134.jpg


BIN
cap/frame135.jpg


BIN
cap/frame136.jpg


BIN
cap/frame137.jpg


BIN
cap/frame138.jpg


BIN
cap/frame139.jpg


BIN
cap/frame14.jpg


BIN
cap/frame140.jpg


BIN
cap/frame141.jpg


BIN
cap/frame142.jpg


BIN
cap/frame143.jpg


BIN
cap/frame144.jpg


BIN
cap/frame145.jpg


BIN
cap/frame146.jpg


BIN
cap/frame147.jpg


BIN
cap/frame148.jpg


BIN
cap/frame149.jpg


BIN
cap/frame15.jpg


BIN
cap/frame150.jpg


BIN
cap/frame151.jpg


BIN
cap/frame152.jpg


BIN
cap/frame153.jpg


BIN
cap/frame154.jpg


BIN
cap/frame155.jpg


BIN
cap/frame156.jpg


BIN
cap/frame157.jpg


BIN
cap/frame158.jpg


BIN
cap/frame159.jpg


BIN
cap/frame16.jpg


BIN
cap/frame160.jpg


BIN
cap/frame161.jpg


BIN
cap/frame162.jpg


BIN
cap/frame163.jpg


BIN
cap/frame164.jpg


BIN
cap/frame165.jpg


BIN
cap/frame166.jpg


BIN
cap/frame167.jpg


BIN
cap/frame168.jpg


BIN
cap/frame169.jpg


BIN
cap/frame17.jpg


BIN
cap/frame170.jpg


BIN
cap/frame171.jpg


BIN
cap/frame172.jpg


BIN
cap/frame173.jpg


BIN
cap/frame174.jpg


BIN
cap/frame175.jpg


BIN
cap/frame176.jpg


BIN
cap/frame177.jpg


BIN
cap/frame178.jpg


BIN
cap/frame179.jpg


BIN
cap/frame18.jpg


BIN
cap/frame180.jpg


BIN
cap/frame181.jpg


+ 0 - 0
cap/frame182.jpg


Some files were not shown because too many files changed in this diff

tum/whitesports - Gogs: Simplico Git Service

Нет описания

class-wp-customize-nav-menu-item-setting.php 27KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  1. <?php
  2. /**
  3. * Customize API: WP_Customize_Nav_Menu_Item_Setting class
  4. *
  5. * @package WordPress
  6. * @subpackage Customize
  7. * @since 4.4.0
  8. */
  9. /**
  10. * Customize Setting to represent a nav_menu.
  11. *
  12. * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and
  13. * the IDs for the nav_menu_items associated with the nav menu.
  14. *
  15. * @since 4.3.0
  16. *
  17. * @see WP_Customize_Setting
  18. */
  19. class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting {
  20. const ID_PATTERN = '/^nav_menu_item\[(?P<id>-?\d+)\]$/';
  21. const POST_TYPE = 'nav_menu_item';
  22. const TYPE = 'nav_menu_item';
  23. /**
  24. * Setting type.
  25. *
  26. * @since 4.3.0
  27. * @var string
  28. */
  29. public $type = self::TYPE;
  30. /**
  31. * Default setting value.
  32. *
  33. * @since 4.3.0
  34. * @var array
  35. *
  36. * @see wp_setup_nav_menu_item()
  37. */
  38. public $default = array(
  39. // The $menu_item_data for wp_update_nav_menu_item().
  40. 'object_id' => 0,
  41. 'object' => '', // Taxonomy name.
  42. 'menu_item_parent' => 0, // A.K.A. menu-item-parent-id; note that post_parent is different, and not included.
  43. 'position' => 0, // A.K.A. menu_order.
  44. 'type' => 'custom', // Note that type_label is not included here.
  45. 'title' => '',
  46. 'url' => '',
  47. 'target' => '',
  48. 'attr_title' => '',
  49. 'description' => '',
  50. 'classes' => '',
  51. 'xfn' => '',
  52. 'status' => 'publish',
  53. 'original_title' => '',
  54. 'nav_menu_term_id' => 0, // This will be supplied as the $menu_id arg for wp_update_nav_menu_item().
  55. '_invalid' => false,
  56. );
  57. /**
  58. * Default transport.
  59. *
  60. * @since 4.3.0
  61. * @since 4.5.0 Default changed to 'refresh'
  62. * @var string
  63. */
  64. public $transport = 'refresh';
  65. /**
  66. * The post ID represented by this setting instance. This is the db_id.
  67. *
  68. * A negative value represents a placeholder ID for a new menu not yet saved.
  69. *
  70. * @since 4.3.0
  71. * @var int
  72. */
  73. public $post_id;
  74. /**
  75. * Storage of pre-setup menu item to prevent wasted calls to wp_setup_nav_menu_item().
  76. *
  77. * @since 4.3.0
  78. * @var array|null
  79. */
  80. protected $value;
  81. /**
  82. * Previous (placeholder) post ID used before creating a new menu item.
  83. *
  84. * This value will be exported to JS via the customize_save_response filter
  85. * so that JavaScript can update the settings to refer to the newly-assigned
  86. * post ID. This value is always negative to indicate it does not refer to
  87. * a real post.
  88. *
  89. * @since 4.3.0
  90. * @var int
  91. *
  92. * @see WP_Customize_Nav_Menu_Item_Setting::update()
  93. * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
  94. */
  95. public $previous_post_id;
  96. /**
  97. * When previewing or updating a menu item, this stores the previous nav_menu_term_id
  98. * which ensures that we can apply the proper filters.
  99. *
  100. * @since 4.3.0
  101. * @var int
  102. */
  103. public $original_nav_menu_term_id;
  104. /**
  105. * Whether or not update() was called.
  106. *
  107. * @since 4.3.0
  108. * @var bool
  109. */
  110. protected $is_updated = false;
  111. /**
  112. * Status for calling the update method, used in customize_save_response filter.
  113. *
  114. * See {@see 'customize_save_response'}.
  115. *
  116. * When status is inserted, the placeholder post ID is stored in $previous_post_id.
  117. * When status is error, the error is stored in $update_error.
  118. *
  119. * @since 4.3.0
  120. * @var string updated|inserted|deleted|error
  121. *
  122. * @see WP_Customize_Nav_Menu_Item_Setting::update()
  123. * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
  124. */
  125. public $update_status;
  126. /**
  127. * Any error object returned by wp_update_nav_menu_item() when setting is updated.
  128. *
  129. * @since 4.3.0
  130. * @var WP_Error
  131. *
  132. * @see WP_Customize_Nav_Menu_Item_Setting::update()
  133. * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
  134. */
  135. public $update_error;
  136. /**
  137. * Constructor.
  138. *
  139. * Any supplied $args override class property defaults.
  140. *
  141. * @since 4.3.0
  142. *
  143. * @throws Exception If $id is not valid for this setting type.
  144. *
  145. * @param WP_Customize_Manager $manager Customizer bootstrap instance.
  146. * @param string $id A specific ID of the setting.
  147. * Can be a theme mod or option name.
  148. * @param array $args Optional. Setting arguments.
  149. */
  150. public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) {
  151. if ( empty( $manager->nav_menus ) ) {
  152. throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' );
  153. }
  154. if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) {
  155. throw new Exception( "Illegal widget setting ID: $id" );
  156. }
  157. $this->post_id = (int) $matches['id'];
  158. add_action( 'wp_update_nav_menu_item', array( $this, 'flush_cached_value' ), 10, 2 );
  159. parent::__construct( $manager, $id, $args );
  160. // Ensure that an initially-supplied value is valid.
  161. if ( isset( $this->value ) ) {
  162. $this->populate_value();
  163. foreach ( array_diff( array_keys( $this->default ), array_keys( $this->value ) ) as $missing ) {
  164. throw new Exception( "Supplied nav_menu_item value missing property: $missing" );
  165. }
  166. }
  167. }
  168. /**
  169. * Clear the cached value when this nav menu item is updated.
  170. *
  171. * @since 4.3.0
  172. *
  173. * @param int $menu_id The term ID for the menu.
  174. * @param int $menu_item_id The post ID for the menu item.
  175. */
  176. public function flush_cached_value( $menu_id, $menu_item_id ) {
  177. unset( $menu_id );
  178. if ( $menu_item_id === $this->post_id ) {
  179. $this->value = null;
  180. }
  181. }
  182. /**
  183. * Get the instance data for a given nav_menu_item setting.
  184. *
  185. * @since 4.3.0
  186. *
  187. * @see wp_setup_nav_menu_item()
  188. *
  189. * @return array|false Instance data array, or false if the item is marked for deletion.
  190. */
  191. public function value() {
  192. if ( $this->is_previewed && get_current_blog_id() === $this->_previewed_blog_id ) {
  193. $undefined = new stdClass(); // Symbol.
  194. $post_value = $this->post_value( $undefined );
  195. if ( $undefined === $post_value ) {
  196. $value = $this->_original_value;
  197. } else {
  198. $value = $post_value;
  199. }
  200. if ( ! empty( $value ) && empty( $value['original_title'] ) ) {
  201. $value['original_title'] = $this->get_original_title( (object) $value );
  202. }
  203. } elseif ( isset( $this->value ) ) {
  204. $value = $this->value;
  205. } else {
  206. $value = false;
  207. // Note that a ID of less than one indicates a nav_menu not yet inserted.
  208. if ( $this->post_id > 0 ) {
  209. $post = get_post( $this->post_id );
  210. if ( $post && self::POST_TYPE === $post->post_type ) {
  211. $is_title_empty = empty( $post->post_title );
  212. $value = (array) wp_setup_nav_menu_item( $post );
  213. if ( $is_title_empty ) {
  214. $value['title'] = '';
  215. }
  216. }
  217. }
  218. if ( ! is_array( $value ) ) {
  219. $value = $this->default;
  220. }
  221. // Cache the value for future calls to avoid having to re-call wp_setup_nav_menu_item().
  222. $this->value = $value;
  223. $this->populate_value();
  224. $value = $this->value;
  225. }
  226. if ( ! empty( $value ) && empty( $value['type_label'] ) ) {
  227. $value['type_label'] = $this->get_type_label( (object) $value );
  228. }
  229. return $value;
  230. }
  231. /**
  232. * Get original title.
  233. *
  234. * @since 4.7.0
  235. *
  236. * @param object $item Nav menu item.
  237. * @return string The original title.
  238. */
  239. protected function get_original_title( $item ) {
  240. $original_title = '';
  241. if ( 'post_type' === $item->type && ! empty( $item->object_id ) ) {
  242. $original_object = get_post( $item->object_id );
  243. if ( $original_object ) {
  244. /** This filter is documented in wp-includes/post-template.php */
  245. $original_title = apply_filters( 'the_title', $original_object->post_title, $original_object->ID );
  246. if ( '' === $original_title ) {
  247. /* translators: %d: ID of a post. */
  248. $original_title = sprintf( __( '#%d (no title)' ), $original_object->ID );
  249. }
  250. }
  251. } elseif ( 'taxonomy' === $item->type && ! empty( $item->object_id ) ) {
  252. $original_term_title = get_term_field( 'name', $item->object_id, $item->object, 'raw' );
  253. if ( ! is_wp_error( $original_term_title ) ) {
  254. $original_title = $original_term_title;
  255. }
  256. } elseif ( 'post_type_archive' === $item->type ) {
  257. $original_object = get_post_type_object( $item->object );
  258. if ( $original_object ) {
  259. $original_title = $original_object->labels->archives;
  260. }
  261. }
  262. $original_title = html_entity_decode( $original_title, ENT_QUOTES, get_bloginfo( 'charset' ) );
  263. return $original_title;
  264. }
  265. /**
  266. * Get type label.
  267. *
  268. * @since 4.7.0
  269. *
  270. * @param object $item Nav menu item.
  271. * @return string The type label.
  272. */
  273. protected function get_type_label( $item ) {
  274. if ( 'post_type' === $item->type ) {
  275. $object = get_post_type_object( $item->object );
  276. if ( $object ) {
  277. $type_label = $object->labels->singular_name;
  278. } else {
  279. $type_label = $item->object;
  280. }
  281. } elseif ( 'taxonomy' === $item->type ) {
  282. $object = get_taxonomy( $item->object );
  283. if ( $object ) {
  284. $type_label = $object->labels->singular_name;
  285. } else {
  286. $type_label = $item->object;
  287. }
  288. } elseif ( 'post_type_archive' === $item->type ) {
  289. $type_label = __( 'Post Type Archive' );
  290. } else {
  291. $type_label = __( 'Custom Link' );
  292. }
  293. return $type_label;
  294. }
  295. /**
  296. * Ensure that the value is fully populated with the necessary properties.
  297. *
  298. * Translates some properties added by wp_setup_nav_menu_item() and removes others.
  299. *
  300. * @since 4.3.0
  301. *
  302. * @see WP_Customize_Nav_Menu_Item_Setting::value()
  303. */
  304. protected function populate_value() {
  305. if ( ! is_array( $this->value ) ) {
  306. return;
  307. }
  308. if ( isset( $this->value['menu_order'] ) ) {
  309. $this->value['position'] = $this->value['menu_order'];
  310. unset( $this->value['menu_order'] );
  311. }
  312. if ( isset( $this->value['post_status'] ) ) {
  313. $this->value['status'] = $this->value['post_status'];
  314. unset( $this->value['post_status'] );
  315. }
  316. if ( ! isset( $this->value['original_title'] ) ) {
  317. $this->value['original_title'] = $this->get_original_title( (object) $this->value );
  318. }
  319. if ( ! isset( $this->value['nav_menu_term_id'] ) && $this->post_id > 0 ) {
  320. $menus = wp_get_post_terms(
  321. $this->post_id,
  322. WP_Customize_Nav_Menu_Setting::TAXONOMY,
  323. array(
  324. 'fields' => 'ids',
  325. )
  326. );
  327. if ( ! empty( $menus ) ) {
  328. $this->value['nav_menu_term_id'] = array_shift( $menus );
  329. } else {
  330. $this->value['nav_menu_term_id'] = 0;
  331. }
  332. }
  333. foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
  334. if ( ! is_int( $this->value[ $key ] ) ) {
  335. $this->value[ $key ] = (int) $this->value[ $key ];
  336. }
  337. }
  338. foreach ( array( 'classes', 'xfn' ) as $key ) {
  339. if ( is_array( $this->value[ $key ] ) ) {
  340. $this->value[ $key ] = implode( ' ', $this->value[ $key ] );
  341. }
  342. }
  343. if ( ! isset( $this->value['title'] ) ) {
  344. $this->value['title'] = '';
  345. }
  346. if ( ! isset( $this->value['_invalid'] ) ) {
  347. $this->value['_invalid'] = false;
  348. $is_known_invalid = (
  349. ( ( 'post_type' === $this->value['type'] || 'post_type_archive' === $this->value['type'] ) && ! post_type_exists( $this->value['object'] ) )
  350. ||
  351. ( 'taxonomy' === $this->value['type'] && ! taxonomy_exists( $this->value['object'] ) )
  352. );
  353. if ( $is_known_invalid ) {
  354. $this->value['_invalid'] = true;
  355. }
  356. }
  357. // Remove remaining properties available on a setup nav_menu_item post object which aren't relevant to the setting value.
  358. $irrelevant_properties = array(
  359. 'ID',
  360. 'comment_count',
  361. 'comment_status',
  362. 'db_id',
  363. 'filter',
  364. 'guid',
  365. 'ping_status',
  366. 'pinged',
  367. 'post_author',
  368. 'post_content',
  369. 'post_content_filtered',
  370. 'post_date',
  371. 'post_date_gmt',
  372. 'post_excerpt',
  373. 'post_mime_type',
  374. 'post_modified',
  375. 'post_modified_gmt',
  376. 'post_name',
  377. 'post_parent',
  378. 'post_password',
  379. 'post_title',
  380. 'post_type',
  381. 'to_ping',
  382. );
  383. foreach ( $irrelevant_properties as $property ) {
  384. unset( $this->value[ $property ] );
  385. }
  386. }
  387. /**
  388. * Handle previewing the setting.
  389. *
  390. * @since 4.3.0
  391. * @since 4.4.0 Added boolean return value.
  392. *
  393. * @see WP_Customize_Manager::post_value()
  394. *
  395. * @return bool False if method short-circuited due to no-op.
  396. */
  397. public function preview() {
  398. if ( $this->is_previewed ) {
  399. return false;
  400. }
  401. $undefined = new stdClass();
  402. $is_placeholder = ( $this->post_id < 0 );
  403. $is_dirty = ( $undefined !== $this->post_value( $undefined ) );
  404. if ( ! $is_placeholder && ! $is_dirty ) {
  405. return false;
  406. }
  407. $this->is_previewed = true;
  408. $this->_original_value = $this->value();
  409. $this->original_nav_menu_term_id = $this->_original_value['nav_menu_term_id'];
  410. $this->_previewed_blog_id = get_current_blog_id();
  411. add_filter( 'wp_get_nav_menu_items', array( $this, 'filter_wp_get_nav_menu_items' ), 10, 3 );
  412. $sort_callback = array( __CLASS__, 'sort_wp_get_nav_menu_items' );
  413. if ( ! has_filter( 'wp_get_nav_menu_items', $sort_callback ) ) {
  414. add_filter( 'wp_get_nav_menu_items', array( __CLASS__, 'sort_wp_get_nav_menu_items' ), 1000, 3 );
  415. }
  416. // @todo Add get_post_metadata filters for plugins to add their data.
  417. return true;
  418. }
  419. /**
  420. * Filters the wp_get_nav_menu_items() result to supply the previewed menu items.
  421. *
  422. * @since 4.3.0
  423. *
  424. * @see wp_get_nav_menu_items()
  425. *
  426. * @param WP_Post[] $items An array of menu item post objects.
  427. * @param WP_Term $menu The menu object.
  428. * @param array $args An array of arguments used to retrieve menu item objects.
  429. * @return WP_Post[] Array of menu item objects.
  430. */
  431. public function filter_wp_get_nav_menu_items( $items, $menu, $args ) {
  432. $this_item = $this->value();
  433. $current_nav_menu_term_id = null;
  434. if ( isset( $this_item['nav_menu_term_id'] ) ) {
  435. $current_nav_menu_term_id = $this_item['nav_menu_term_id'];
  436. unset( $this_item['nav_menu_term_id'] );
  437. }
  438. $should_filter = (
  439. $menu->term_id === $this->original_nav_menu_term_id
  440. ||
  441. $menu->term_id === $current_nav_menu_term_id
  442. );
  443. if ( ! $should_filter ) {
  444. return $items;
  445. }
  446. // Handle deleted menu item, or menu item moved to another menu.
  447. $should_remove = (
  448. false === $this_item
  449. ||
  450. ( isset( $this_item['_invalid'] ) && true === $this_item['_invalid'] )
  451. ||
  452. (
  453. $this->original_nav_menu_term_id === $menu->term_id
  454. &&
  455. $current_nav_menu_term_id !== $this->original_nav_menu_term_id
  456. )
  457. );
  458. if ( $should_remove ) {
  459. $filtered_items = array();
  460. foreach ( $items as $item ) {
  461. if ( $item->db_id !== $this->post_id ) {
  462. $filtered_items[] = $item;
  463. }
  464. }
  465. return $filtered_items;
  466. }
  467. $mutated = false;
  468. $should_update = (
  469. is_array( $this_item )
  470. &&
  471. $current_nav_menu_term_id === $menu->term_id
  472. );
  473. if ( $should_update ) {
  474. foreach ( $items as $item ) {
  475. if ( $item->db_id === $this->post_id ) {
  476. foreach ( get_object_vars( $this->value_as_wp_post_nav_menu_item() ) as $key => $value ) {
  477. $item->$key = $value;
  478. }
  479. $mutated = true;
  480. }
  481. }
  482. // Not found so we have to append it..
  483. if ( ! $mutated ) {
  484. $items[] = $this->value_as_wp_post_nav_menu_item();
  485. }
  486. }
  487. return $items;
  488. }
  489. /**
  490. * Re-apply the tail logic also applied on $items by wp_get_nav_menu_items().
  491. *
  492. * @since 4.3.0
  493. *
  494. * @see wp_get_nav_menu_items()
  495. *
  496. * @param WP_Post[] $items An array of menu item post objects.
  497. * @param WP_Term $menu The menu object.
  498. * @param array $args An array of arguments used to retrieve menu item objects.
  499. * @return WP_Post[] Array of menu item objects.
  500. */
  501. public static function sort_wp_get_nav_menu_items( $items, $menu, $args ) {
  502. // @todo We should probably re-apply some constraints imposed by $args.
  503. unset( $args['include'] );
  504. // Remove invalid items only in front end.
  505. if ( ! is_admin() ) {
  506. $items = array_filter( $items, '_is_valid_nav_menu_item' );
  507. }
  508. if ( ARRAY_A === $args['output'] ) {
  509. $items = wp_list_sort(
  510. $items,
  511. array(
  512. $args['output_key'] => 'ASC',
  513. )
  514. );
  515. $i = 1;
  516. foreach ( $items as $k => $item ) {
  517. $items[ $k ]->{$args['output_key']} = $i++;
  518. }
  519. }
  520. return $items;
  521. }
  522. /**
  523. * Get the value emulated into a WP_Post and set up as a nav_menu_item.
  524. *
  525. * @since 4.3.0
  526. *
  527. * @return WP_Post With wp_setup_nav_menu_item() applied.
  528. */
  529. public function value_as_wp_post_nav_menu_item() {
  530. $item = (object) $this->value();
  531. unset( $item->nav_menu_term_id );
  532. $item->post_status = $item->status;
  533. unset( $item->status );
  534. $item->post_type = 'nav_menu_item';
  535. $item->menu_order = $item->position;
  536. unset( $item->position );
  537. if ( empty( $item->original_title ) ) {
  538. $item->original_title = $this->get_original_title( $item );
  539. }
  540. if ( empty( $item->title ) && ! empty( $item->original_title ) ) {
  541. $item->title = $item->original_title;
  542. }
  543. if ( $item->title ) {
  544. $item->post_title = $item->title;
  545. }
  546. // 'classes' should be an array, as in wp_setup_nav_menu_item().
  547. if ( isset( $item->classes ) && is_scalar( $item->classes ) ) {
  548. $item->classes = explode( ' ', $item->classes );
  549. }
  550. $item->ID = $this->post_id;
  551. $item->db_id = $this->post_id;
  552. $post = new WP_Post( (object) $item );
  553. if ( empty( $post->post_author ) ) {
  554. $post->post_author = get_current_user_id();
  555. }
  556. if ( ! isset( $post->type_label ) ) {
  557. $post->type_label = $this->get_type_label( $post );
  558. }
  559. // Ensure nav menu item URL is set according to linked object.
  560. if ( 'post_type' === $post->type && ! empty( $post->object_id ) ) {
  561. $post->url = get_permalink( $post->object_id );
  562. } elseif ( 'taxonomy' === $post->type && ! empty( $post->object ) && ! empty( $post->object_id ) ) {
  563. $post->url = get_term_link( (int) $post->object_id, $post->object );
  564. } elseif ( 'post_type_archive' === $post->type && ! empty( $post->object ) ) {
  565. $post->url = get_post_type_archive_link( $post->object );
  566. }
  567. if ( is_wp_error( $post->url ) ) {
  568. $post->url = '';
  569. }
  570. /** This filter is documented in wp-includes/nav-menu.php */
  571. $post->attr_title = apply_filters( 'nav_menu_attr_title', $post->attr_title );
  572. /** This filter is documented in wp-includes/nav-menu.php */
  573. $post->description = apply_filters( 'nav_menu_description', wp_trim_words( $post->description, 200 ) );
  574. /** This filter is documented in wp-includes/nav-menu.php */
  575. $post = apply_filters( 'wp_setup_nav_menu_item', $post );
  576. return $post;
  577. }
  578. /**
  579. * Sanitize an input.
  580. *
  581. * Note that parent::sanitize() erroneously does wp_unslash() on $value, but
  582. * we remove that in this override.
  583. *
  584. * @since 4.3.0
  585. * @since 5.9.0 Renamed `$menu_item_value` to `$value` for PHP 8 named parameter support.
  586. *
  587. * @param array $value The menu item value to sanitize.
  588. * @return array|false|null|WP_Error Null or WP_Error if an input isn't valid. False if it is marked for deletion.
  589. * Otherwise the sanitized value.
  590. */
  591. public function sanitize( $value ) {
  592. // Restores the more descriptive, specific name for use within this method.
  593. $menu_item_value = $value;
  594. // Menu is marked for deletion.
  595. if ( false === $menu_item_value ) {
  596. return $menu_item_value;
  597. }
  598. // Invalid.
  599. if ( ! is_array( $menu_item_value ) ) {
  600. return null;
  601. }
  602. $default = array(
  603. 'object_id' => 0,
  604. 'object' => '',
  605. 'menu_item_parent' => 0,
  606. 'position' => 0,
  607. 'type' => 'custom',
  608. 'title' => '',
  609. 'url' => '',
  610. 'target' => '',
  611. 'attr_title' => '',
  612. 'description' => '',
  613. 'classes' => '',
  614. 'xfn' => '',
  615. 'status' => 'publish',
  616. 'original_title' => '',
  617. 'nav_menu_term_id' => 0,
  618. '_invalid' => false,
  619. );
  620. $menu_item_value = array_merge( $default, $menu_item_value );
  621. $menu_item_value = wp_array_slice_assoc( $menu_item_value, array_keys( $default ) );
  622. $menu_item_value['position'] = (int) $menu_item_value['position'];
  623. foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
  624. // Note we need to allow negative-integer IDs for previewed objects not inserted yet.
  625. $menu_item_value[ $key ] = (int) $menu_item_value[ $key ];
  626. }
  627. foreach ( array( 'type', 'object', 'target' ) as $key ) {
  628. $menu_item_value[ $key ] = sanitize_key( $menu_item_value[ $key ] );
  629. }
  630. foreach ( array( 'xfn', 'classes' ) as $key ) {
  631. $value = $menu_item_value[ $key ];
  632. if ( ! is_array( $value ) ) {
  633. $value = explode( ' ', $value );
  634. }
  635. $menu_item_value[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) );
  636. }
  637. $menu_item_value['original_title'] = sanitize_text_field( $menu_item_value['original_title'] );
  638. // Apply the same filters as when calling wp_insert_post().
  639. /** This filter is documented in wp-includes/post.php */
  640. $menu_item_value['title'] = wp_unslash( apply_filters( 'title_save_pre', wp_slash( $menu_item_value['title'] ) ) );
  641. /** This filter is documented in wp-includes/post.php */
  642. $menu_item_value['attr_title'] = wp_unslash( apply_filters( 'excerpt_save_pre', wp_slash( $menu_item_value['attr_title'] ) ) );
  643. /** This filter is documented in wp-includes/post.php */
  644. $menu_item_value['description'] = wp_unslash( apply_filters( 'content_save_pre', wp_slash( $menu_item_value['description'] ) ) );
  645. if ( '' !== $menu_item_value['url'] ) {
  646. $menu_item_value['url'] = esc_url_raw( $menu_item_value['url'] );
  647. if ( '' === $menu_item_value['url'] ) {
  648. return new WP_Error( 'invalid_url', __( 'Invalid URL.' ) ); // Fail sanitization if URL is invalid.
  649. }
  650. }
  651. if ( 'publish' !== $menu_item_value['status'] ) {
  652. $menu_item_value['status'] = 'draft';
  653. }
  654. $menu_item_value['_invalid'] = (bool) $menu_item_value['_invalid'];
  655. /** This filter is documented in wp-includes/class-wp-customize-setting.php */
  656. return apply_filters( "customize_sanitize_{$this->id}", $menu_item_value, $this );
  657. }
  658. /**
  659. * Creates/updates the nav_menu_item post for this setting.
  660. *
  661. * Any created menu items will have their assigned post IDs exported to the client
  662. * via the {@see 'customize_save_response'} filter. Likewise, any errors will be
  663. * exported to the client via the customize_save_response() filter.
  664. *
  665. * To delete a menu, the client can send false as the value.
  666. *
  667. * @since 4.3.0
  668. *
  669. * @see wp_update_nav_menu_item()
  670. *
  671. * @param array|false $value The menu item array to update. If false, then the menu item will be deleted
  672. * entirely. See WP_Customize_Nav_Menu_Item_Setting::$default for what the value
  673. * should consist of.
  674. * @return null|void
  675. */
  676. protected function update( $value ) {
  677. if ( $this->is_updated ) {
  678. return;
  679. }
  680. $this->is_updated = true;
  681. $is_placeholder = ( $this->post_id < 0 );
  682. $is_delete = ( false === $value );
  683. // Update the cached value.
  684. $this->value = $value;
  685. add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) );
  686. if ( $is_delete ) {
  687. // If the current setting post is a placeholder, a delete request is a no-op.
  688. if ( $is_placeholder ) {
  689. $this->update_status = 'deleted';
  690. } else {
  691. $r = wp_delete_post( $this->post_id, true );
  692. if ( false === $r ) {
  693. $this->update_error = new WP_Error( 'delete_failure' );
  694. $this->update_status = 'error';
  695. } else {
  696. $this->update_status = 'deleted';
  697. }
  698. // @todo send back the IDs for all associated nav menu items deleted, so these settings (and controls) can be removed from Customizer?
  699. }
  700. } else {
  701. // Handle saving menu items for menus that are being newly-created.
  702. if ( $value['nav_menu_term_id'] < 0 ) {
  703. $nav_menu_setting_id = sprintf( 'nav_menu[%s]', $value['nav_menu_term_id'] );
  704. $nav_menu_setting = $this->manager->get_setting( $nav_menu_setting_id );
  705. if ( ! $nav_menu_setting || ! ( $nav_menu_setting instanceof WP_Customize_Nav_Menu_Setting ) ) {
  706. $this->update_status = 'error';
  707. $this->update_error = new WP_Error( 'unexpected_nav_menu_setting' );
  708. return;
  709. }
  710. if ( false === $nav_menu_setting->save() ) {
  711. $this->update_status = 'error';
  712. $this->update_error = new WP_Error( 'nav_menu_setting_failure' );
  713. return;
  714. }
  715. if ( (int) $value['nav_menu_term_id'] !== $nav_menu_setting->previous_term_id ) {
  716. $this->update_status = 'error';
  717. $this->update_error = new WP_Error( 'unexpected_previous_term_id' );
  718. return;
  719. }
  720. $value['nav_menu_term_id'] = $nav_menu_setting->term_id;
  721. }
  722. // Handle saving a nav menu item that is a child of a nav menu item being newly-created.
  723. if ( $value['menu_item_parent'] < 0 ) {
  724. $parent_nav_menu_item_setting_id = sprintf( 'nav_menu_item[%s]', $value['menu_item_parent'] );
  725. $parent_nav_menu_item_setting = $this->manager->get_setting( $parent_nav_menu_item_setting_id );
  726. if ( ! $parent_nav_menu_item_setting || ! ( $parent_nav_menu_item_setting instanceof WP_Customize_Nav_Menu_Item_Setting ) ) {
  727. $this->update_status = 'error';
  728. $this->update_error = new WP_Error( 'unexpected_nav_menu_item_setting' );
  729. return;
  730. }
  731. if ( false === $parent_nav_menu_item_setting->save() ) {
  732. $this->update_status = 'error';
  733. $this->update_error = new WP_Error( 'nav_menu_item_setting_failure' );
  734. return;
  735. }
  736. if ( (int) $value['menu_item_parent'] !== $parent_nav_menu_item_setting->previous_post_id ) {
  737. $this->update_status = 'error';
  738. $this->update_error = new WP_Error( 'unexpected_previous_post_id' );
  739. return;
  740. }
  741. $value['menu_item_parent'] = $parent_nav_menu_item_setting->post_id;
  742. }
  743. // Insert or update menu.
  744. $menu_item_data = array(
  745. 'menu-item-object-id' => $value['object_id'],
  746. 'menu-item-object' => $value['object'],
  747. 'menu-item-parent-id' => $value['menu_item_parent'],
  748. 'menu-item-position' => $value['position'],
  749. 'menu-item-type' => $value['type'],
  750. 'menu-item-title' => $value['title'],
  751. 'menu-item-url' => $value['url'],
  752. 'menu-item-description' => $value['description'],
  753. 'menu-item-attr-title' => $value['attr_title'],
  754. 'menu-item-target' => $value['target'],
  755. 'menu-item-classes' => $value['classes'],
  756. 'menu-item-xfn' => $value['xfn'],
  757. 'menu-item-status' => $value['status'],
  758. );
  759. $r = wp_update_nav_menu_item(
  760. $value['nav_menu_term_id'],
  761. $is_placeholder ? 0 : $this->post_id,
  762. wp_slash( $menu_item_data )
  763. );
  764. if ( is_wp_error( $r ) ) {
  765. $this->update_status = 'error';
  766. $this->update_error = $r;
  767. } else {
  768. if ( $is_placeholder ) {
  769. $this->previous_post_id = $this->post_id;
  770. $this->post_id = $r;
  771. $this->update_status = 'inserted';
  772. } else {
  773. $this->update_status = 'updated';
  774. }
  775. }
  776. }
  777. }
  778. /**
  779. * Export data for the JS client.
  780. *
  781. * @since 4.3.0
  782. *
  783. * @see WP_Customize_Nav_Menu_Item_Setting::update()
  784. *
  785. * @param array $data Additional information passed back to the 'saved' event on `wp.customize`.
  786. * @return array Save response data.
  787. */
  788. public function amend_customize_save_response( $data ) {
  789. if ( ! isset( $data['nav_menu_item_updates'] ) ) {
  790. $data['nav_menu_item_updates'] = array();
  791. }
  792. $data['nav_menu_item_updates'][] = array(
  793. 'post_id' => $this->post_id,
  794. 'previous_post_id' => $this->previous_post_id,
  795. 'error' => $this->update_error ? $this->update_error->get_error_code() : null,
  796. 'status' => $this->update_status,
  797. );
  798. return $data;
  799. }
  800. }