pace to upload. %s KB needed.' ), number_format( ( $file_size - $space_left ) / KB_IN_BYTES ) ), array( 'status' => 400 ) ); } if ( $file_size > ( KB_IN_BYTES * get_site_option( 'fileupload_maxk', 1500 ) ) ) { return new WP_Error( 'rest_upload_file_too_big', /* translators: %s: Maximum allowed file size in kilobytes. */ sprintf( __( 'This file is too big. Files must be less than %s KB in size.' ), get_site_option( 'fileupload_maxk', 1500 ) ), array( 'status' => 400 ) ); } // Include multisite admin functions to get access to upload_is_user_over_quota(). require_once ABSPATH . 'wp-admin/includes/ms.php'; if ( upload_is_user_over_quota( false ) ) { return new WP_Error( 'rest_upload_user_quota_exceeded', __( 'You have used your space quota. Please delete files before uploading.' ), array( 'status' => 400 ) ); } return true; } /** * Gets the request args for the edit item route. * * @since 5.5.0 * @since 6.9.0 Adds flips capability and editable fields for the newly-created attachment post. * * @return array */ protected function get_edit_media_item_args() { $args = array( 'src' => array( 'description' => __( 'URL to the edited image file.' ), 'type' => 'string', 'format' => 'uri', 'required' => true, ), // The `modifiers` param takes precedence over the older format. 'modifiers' => array( 'description' => __( 'Array of image edits.' ), 'type' => 'array', 'minItems' => 1, 'items' => array( 'description' => __( 'Image edit.' ), 'type' => 'object', 'required' => array( 'type', 'args', ), 'oneOf' => array( array( 'title' => __( 'Flip' ), 'properties' => array( 'type' => array( 'description' => __( 'Flip type.' ), 'type' => 'string', 'enum' => array( 'flip' ), ), 'args' => array( 'description' => __( 'Flip arguments.' ), 'type' => 'object', 'required' => array( 'flip', ), 'properties' => array( 'flip' => array( 'description' => __( 'Flip direction.' ), 'type' => 'object', 'required' => array( 'horizontal', 'vertical', ), 'properties' => array( 'horizontal' => array( 'description' => __( 'Whether to flip in the horizontal direction.' ), 'type' => 'boolean', ), 'vertical' => array( 'description' => __( 'Whether to flip in the vertical direction.' ), 'type' => 'boolean', ), ), ), ), ), ), ), array( 'title' => __( 'Rotation' ), 'properties' => array( 'type' => array( 'description' => __( 'Rotation type.' ), 'type' => 'string', 'enum' => array( 'rotate' ), ), 'args' => array( 'description' => __( 'Rotation arguments.' ), 'type' => 'object', 'required' => array( 'angle', ), 'properties' => array( 'angle' => array( 'description' => __( 'Angle to rotate clockwise in degrees.' ), 'type' => 'number', ), ), ), ), ), array( 'title' => __( 'Crop' ), 'properties' => array( 'type' => array( 'description' => __( 'Crop type.' ), 'type' => 'string', 'enum' => array( 'crop' ), ), 'args' => array( 'description' => __( 'Crop arguments.' ), 'type' => 'object', 'required' => array( 'left', 'top', 'width', 'height', ), 'properties' => array( 'left' => array( 'description' => __( 'Horizontal position from the left to begin the crop as a percentage of the image width.' ), 'type' => 'number', ), 'top' => array( 'description' => __( 'Vertical position from the top to begin the crop as a percentage of the image height.' ), 'type' => 'number', ), 'width' => array( 'description' => __( 'Width of the crop as a percentage of the image width.' ), 'type' => 'number', ), 'height' => array( 'description' => __( 'Height of the crop as a percentage of the image height.' ), 'type' => 'number', ), ), ), ), ), ), ), ), 'rotation' => array( 'description' => __( 'The amount to rotate the image clockwise in degrees. DEPRECATED: Use `modifiers` instead.' ), 'type' => 'integer', 'minimum' => 0, 'exclusiveMinimum' => true, 'maximum' => 360, 'exclusiveMaximum' => true, ), 'x' => array( 'description' => __( 'As a percentage of the image, the x position to start the crop from. DEPRECATED: Use `modifiers` instead.' ), 'type' => 'number', 'minimum' => 0, 'maximum' => 100, ), 'y' => array( 'description' => __( 'As a percentage of the image, the y position to start the crop from. DEPRECATED: Use `modifiers` instead.' ), 'type' => 'number', 'minimum' => 0, 'maximum' => 100, ), 'width' => array( 'description' => __( 'As a percentage of the image, the width to crop the image to. DEPRECATED: Use `modifiers` instead.' ), 'type' => 'number', 'minimum' => 0, 'maximum' => 100, ), 'height' => array( 'description' => __( 'As a percentage of the image, the height to crop the image to. DEPRECATED: Use `modifiers` instead.' ), 'type' => 'number', 'minimum' => 0, 'maximum' => 100, ), ); /* * Get the args based on the post schema. This calls `rest_get_endpoint_args_for_schema()`, * which also takes care of sanitization and validation. */ $update_item_args = $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ); if ( isset( $update_item_args['caption'] ) ) { $args['caption'] = $update_item_args['caption']; } if ( isset( $update_item_args['description'] ) ) { $args['description'] = $update_item_args['description']; } if ( isset( $update_item_args['title'] ) ) { $args['title'] = $update_item_args['title']; } if ( isset( $update_item_args['post'] ) ) { $args['post'] = $update_item_args['post']; } if ( isset( $update_item_args['alt_text'] ) ) { $args['alt_text'] = $update_item_args['alt_text']; } return $args; } /** * Gets the attachment's original file name. * * @since 7.0.0 * * @param int $attachment_id Attachment ID. * @return string|null Attachment file name, or null if not found. */ protected function get_attachment_filename( int $attachment_id ): ?string { $path = wp_get_original_image_path( $attachment_id ); if ( $path ) { return wp_basename( $path ); } $path = get_attached_file( $attachment_id ); if ( $path ) { return wp_basename( $path ); } return null; } /** * Gets the attachment's file size in bytes. * * @since 7.0.0 * * @param int $attachment_id Attachment ID. * @return int|null Attachment file size in bytes, or null if not available. * @phpstan-return non-negative-int|null */ protected function get_attachment_filesize( int $attachment_id ): ?int { $meta = wp_get_attachment_metadata( $attachment_id ); if ( isset( $meta['filesize'] ) && is_numeric( $meta['filesize'] ) && $meta['filesize'] > 0 ) { return (int) $meta['filesize']; } $original_path = wp_get_original_image_path( $attachment_id ); $attached_file = $original_path ? $original_path : get_attached_file( $attachment_id ); if ( is_string( $attached_file ) && is_readable( $attached_file ) ) { return wp_filesize( $attached_file ); } return null; } /** * Checks if a given request has access to sideload a file. * * Sideloading a file for an existing attachment * requires both update and create permissions. * * @since 7.1.0 * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise. */ public function sideload_item_permissions_check( $request ) { return $this->edit_media_item_permissions_check( $request ); } /** * Validates an image size name, or an array of names sharing a single file. * * Shared by the sideload endpoint, which names the size a file is produced * for, and the finalize endpoint, which names the size each submitted entry * is stored under. Both need the same set, and finalize accepts a payload of * its own rather than one this class produced, so leaving it unconstrained * there would let a submission write an arbitrary key into the metadata * 'sizes' array or route a file into a branch it was never produced for. * * @since 7.1.0 * * @param mixed $value The image size name, or an array of names. * @param string $param Parameter name, used in the error messages. * @return true|WP_Error True when every name is valid, WP_Error otherwise. */ private static function validate_image_size_names( $value, string $param ) { $special_sizes = self::get_special_image_sizes(); $regular_sizes = array_values( array_diff( array_merge( array_keys( wp_get_registered_image_subsizes() ), // Not a registered sub-size, but stored as an ordinary // entry in the metadata 'sizes' array (PDF thumbnails). array( 'full' ) ), $special_sizes ) ); if ( is_string( $value ) ) { $items = array( $value ); $valid_sizes = array_merge( $regular_sizes, $special_sizes ); } elseif ( is_array( $value ) ) { /** * An array registers one sideloaded file under several size names, * which only makes sense for regular sub-sizes: each special size * names a single file with its own handling in * {@see self::sideload_item()} and its own metadata key in * {@see self::finalize_item()}. Rejecting them here is what lets the * array branches in both methods treat an array as regular sizes. */ $items = $value; $valid_sizes = $regular_sizes; } else { return new WP_Error( 'rest_invalid_type', /* translators: %s: Parameter name. */ sprintf( __( '%s must be a string or an array of strings.' ), $param ) ); } foreach ( $items as $item ) { if ( ! in_array( $item, $valid_sizes, true ) ) { return new WP_Error( 'rest_not_in_enum', /* translators: %s: Parameter name. */ sprintf( __( '%s contains an invalid image size.' ), $param ) ); } } return true; } /** * Returns the image size names which name a single file rather than a sub-size. * * Each of these is handled on its own in {@see self::sideload_item()} and stored * under its own key by {@see self::finalize_item()}, so unlike a regular * sub-size none of them may appear in an array of names sharing one file. * * @since 7.1.0 * * @return string[] Special image size names. * * @phpstan-return non-empty-list */ private static function get_special_image_sizes(): array { return array( 'original', 'scaled', // Source-format original (e.g. the HEIC kept alongside its JPEG derivative). self::IMAGE_SIZE_SOURCE_ORIGINAL, // Converted-video companions for an animated GIF (the MP4/WebM and its poster). 'animated_video', 'animated_video_poster', ); } /** * Validates that uploaded image dimensions are appropriate for the specified image size. * * @since 7.1.0 * * @param int $width Uploaded image width. * @param int $height Uploaded image height. * @param string $image_size The target image size name. * @param int $attachment_id The attachment ID. * @return true|WP_Error True if valid, WP_Error if invalid. */ private function validate_image_dimensions( int $width, int $height, string $image_size, int $attachment_id ) { // All image sizes require positive dimensions. if ( $width <= 0 || $height <= 0 ) { return new WP_Error( 'rest_upload_invalid_dimensions', __( 'Uploaded image must have positive dimensions.' ), array( 'status' => 400 ) ); } /* * 'original' size: the full-size image that replaces the main file (see * sideload_item()/finalize_item()). The endpoint expects any EXIF * orientation to be applied to the image already, which can swap width * and height, so the dimensions must match the stored dimensions or be * their transpose. */ if ( 'original' === $image_size ) { $metadata = wp_get_attachment_metadata( $attachment_id, true ); if ( is_array( $metadata ) && isset( $metadata['width'], $metadata['height'] ) ) { $expected_width = (int) $metadata['width']; $expected_height = (int) $metadata['height']; $matches_dimensions = $width === $expected_width && $height === $expected_height; $transposes_dimensions = $width === $expected_height && $height === $expected_width; if ( ! $matches_dimensions && ! $transposes_dimensions ) { return new WP_Error( 'rest_upload_dimension_mismatch', sprintf( /* translators: 1: Actual width, 2: actual height, 3: expected width, 4: expected height. */ __( 'Uploaded image dimensions (%1$dx%2$d) do not match original image dimensions (%3$dx%4$d).' ), $width, $height, $expected_width, $expected_height ), array( 'status' => 400 ) ); } } return true; } // 'full' size (PDF thumbnails) and 'scaled': no further constraints. if ( in_array( $image_size, array( 'full', 'scaled' ), true ) ) { return true; } /* * 'animated_video_poster' companion: a static poster image for the * converted video. It is a real image (so it has positive dimensions) * but is not a registered sub-size, so it has no dimension constraint. */ if ( 'animated_video_poster' === $image_size ) { return true; } // Regular image sizes: validate against registered size constraints. $registered_sizes = wp_get_registered_image_subsizes(); if ( ! isset( $registered_sizes[ $image_size ] ) ) { return new WP_Error( 'rest_upload_unknown_size', __( 'Unknown image size.' ), array( 'status' => 400 ) ); } $size_data = $registered_sizes[ $image_size ]; $max_width = (int) $size_data['width']; $max_height = (int) $size_data['height']; // Validate dimensions don't exceed the registered size maximums. // Allow 1px tolerance for rounding differences. $tolerance = 1; if ( $this->dimension_exceeds_max( $width, $max_width, $tolerance ) ) { return new WP_Error( 'rest_upload_dimension_mismatch', sprintf( /* translators: 1: Image size name, 2: maximum width, 3: actual width. */ __( 'Uploaded image width (%3$d) exceeds maximum for "%1$s" size (%2$d).' ), $image_size, $max_width, $width ), array( 'status' => 400 ) ); } if ( $this->dimension_exceeds_max( $height, $max_height, $tolerance ) ) { return new WP_Error( 'rest_upload_dimension_mismatch', sprintf( /* translators: 1: Image size name, 2: maximum height, 3: actual height. */ __( 'Uploaded image height (%3$d) exceeds maximum for "%1$s" size (%2$d).' ), $image_size, $max_height, $height ), array( 'status' => 400 ) ); } return true; } /** * Checks whether a dimension exceeds the maximum allowed value. * * A maximum of zero means the dimension is unconstrained. * * @since 7.1.0 * * @param int $value The actual dimension in pixels. * @param int $max The maximum allowed dimension in pixels. Zero means no constraint. * @param int $tolerance Pixel tolerance allowed for rounding differences. * @return bool True if the value exceeds the maximum plus tolerance. */ private function dimension_exceeds_max( int $value, int $max, int $tolerance ): bool { return $max > 0 && $value > $max + $tolerance; } /** * Side-loads a media file without creating a new attachment. * * @since 7.1.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure. */ public function sideload_item( WP_REST_Request $request ) { $attachment_id = (int) $request['id']; $post = $this->get_post( $attachment_id ); if ( is_wp_error( $post ) ) { return $post; } if ( ! wp_attachment_is_image( $post ) && ! wp_attachment_is( 'pdf', $post ) ) { return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID. Only images and PDFs can be sideloaded.' ), array( 'status' => 400 ) ); } /* * Sideloaded files are placed in the same directory as the attachment * they extend, because the file names produced here are later resolved * against that directory. An attachment stored outside the uploads * directory has no such directory to use, so there is nowhere the names * this would produce could resolve. */ $attached_file = get_attached_file( $attachment_id, true ); $subdir = is_string( $attached_file ) && '' !== $attached_file ? $this->get_attachment_upload_subdir( $attached_file ) : null; if ( ! is_string( $attached_file ) || '' === $attached_file || null === $subdir ) { return new WP_Error( 'rest_sideload_attachment_not_in_uploads', __( 'The attachment is not stored in the uploads directory, so a file cannot be sideloaded for it.' ), array( 'status' => 403 ) ); } if ( false === $request['convert_format'] ) { // Prevent image conversion as that is done client-side. add_filter( 'image_editor_output_format', '__return_empty_array', 100 ); } // Get the file via $_FILES or raw data. $files = $request->get_file_params(); $headers = $request->get_headers(); /* * wp_unique_filename() will always add numeric suffix if the name looks like a sub-size to avoid conflicts. * See /wp-includes/functions.php. * With the following filter we can work around this safeguard. */ $attachment_filename = wp_basename( $attached_file ); $filter_filename = static function ( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number ) use ( $attachment_filename ) { return self::filter_wp_unique_filename( $filename, $dir, $number, $attachment_filename ); }; add_filter( 'wp_unique_filename', $filter_filename, 10, 6 ); // Pin the upload to the attachment's own directory, rather than deriving // it from the parent post's date as media_handle_upload() does for a // brand new upload. See the note above where $subdir is resolved. $filter_upload_dir = static function ( $uploads ) use ( $subdir ) { if ( is_array( $uploads ) && isset( $uploads['basedir'], $uploads['baseurl'] ) && is_string( $uploads['basedir'] ) && is_string( $uploads['baseurl'] ) ) { $uploads['subdir'] = $subdir; $uploads['path'] = $uploads['basedir'] . $subdir; $uploads['url'] = $uploads['baseurl'] . $subdir; } return $uploads; }; add_filter( 'upload_dir', $filter_upload_dir, 100 ); if ( ! empty( $files ) ) { $file = $this->upload_from_file( $files, $headers ); } else { $file = $this->upload_from_data( $request->get_body(), $headers ); } remove_filter( 'wp_unique_filename', $filter_filename ); remove_filter( 'image_editor_output_format', '__return_empty_array', 100 ); remove_filter( 'upload_dir', $filter_upload_dir, 100 ); if ( is_wp_error( $file ) ) { return $file; } $type = $file['type']; $path = $file['file']; /** @var non-empty-string|non-empty-list $image_size */ $image_size = $request['image_size']; /* * Validate raster sub-sizes before storing them. Two companion sizes * are exempt because wp_getimagesize() may not be able to read the * file at all: the 'animated_video' companion of an animated GIF is a * video (MP4/WebM), and a source-format original (e.g. a HEIC or JXL * kept next to its JPEG derivative) may be an unreadable format. Their * dimensions are neither validated nor recorded. The * 'animated_video_poster' companion is a real image, so it is still * read and rejected if unreadable; validate_image_dimensions() skips * only the registered-size constraint for it. */ $skip_dimension_read = self::IMAGE_SIZE_SOURCE_ORIGINAL === $image_size || 'animated_video' === $image_size; $size = false; if ( ! $skip_dimension_read ) { /* * Read the dimensions up front. A file whose dimensions cannot be * read is corrupted or an unsupported format and must be rejected * rather than silently stored with zero dimensions. */ $size = wp_getimagesize( $path ); if ( ! $size ) { // Clean up the uploaded file. wp_delete_file( $path ); return new WP_Error( 'rest_upload_invalid_image', __( 'Could not read image dimensions. The file may be corrupted or an unsupported format.' ), array( 'status' => 400 ) ); } /* * Validate the dimensions against every size the file is being * registered under. An array $image_size shares one file among * several registered sizes, so the file has to satisfy each of * them; validating only the scalar case would let a name wrapped * in a one-element array skip the constraint entirely. */ foreach ( (array) $image_size as $size_name ) { $validation = $this->validate_image_dimensions( $size[0], $size[1], $size_name, $attachment_id ); if ( is_wp_error( $validation ) ) { // Clean up the uploaded file. wp_delete_file( $path ); return $validation; } } } // Build sub-size data to return to the client. // The client accumulates these and sends them all to the finalize // endpoint, which writes the metadata in a single operation. This // avoids the read-modify-write race that concurrent sideloads for the // same attachment would otherwise hit. $sub_size_data = array( 'image_size' => $image_size, ); if ( is_array( $image_size ) ) { /** * Multiple registered sizes share these dimensions, so a single * sideloaded file is reused for all of them. Arrays only carry * regular sub-sizes; the special keys below are always scalar * (ref. {@see self::get_special_image_sizes()}). Those never skip * the read above, so $size already holds the dimensions. */ $sub_size_data['width'] = $size ? $size[0] : 0; $sub_size_data['height'] = $size ? $size[1] : 0; $sub_size_data['file'] = wp_basename( $path ); $sub_size_data['mime_type'] = $type; $sub_size_data['filesize'] = wp_filesize( $path ); } elseif ( self::IMAGE_SIZE_SOURCE_ORIGINAL === $image_size ) { /* * Source-format original (e.g. the HEIC kept next to its JPEG * derivative). Record the filename so finalize_item can store it * under the dedicated source-image meta key. */ $sub_size_data['file'] = wp_basename( $path ); } elseif ( 'animated_video' === $image_size || 'animated_video_poster' === $image_size ) { /* * Converted-video companion of an animated GIF (the MP4/WebM or * its static first-frame poster). Record the filename so * finalize_item can store it under its dedicated meta key. */ $sub_size_data['file'] = wp_basename( $path ); } elseif ( 'scaled' === $image_size || 'original' === $image_size ) { /* * 'scaled' and 'original' both replace the attachment's main file * with the supplied image and keep the file being replaced as * `original_image`, which is the untouched upload. A 'scaled' * image is downsized and an 'original' image has any EXIF * orientation already applied. This is the same swap WordPress * makes when it scales or rotates an image on upload; see * _wp_image_meta_replace_original(). */ $sub_size_data['original_image'] = $attachment_filename; // Validate the supplied image before updating the attached file. // $size was read above: neither of these sizes skips that read. $filesize = wp_filesize( $path ); if ( ! $size || ! $filesize ) { // Clean up the uploaded file, which nothing references yet. wp_delete_file( $path ); return new WP_Error( 'rest_sideload_invalid_image', __( 'Unable to read the sideloaded image file.' ), array( 'status' => 500 ) ); } // Update the attached file to point to the supplied image. // This writes to _wp_attached_file meta, not _wp_attachment_metadata. if ( $attached_file !== $path && ! update_attached_file( $attachment_id, $path ) ) { // Clean up the uploaded file, which nothing references yet. wp_delete_file( $path ); return new WP_Error( 'rest_sideload_update_attached_file_failed', __( 'Unable to update the attached file for this attachment.' ), array( 'status' => 500 ) ); } $sub_size_data['width'] = $size[0]; $sub_size_data['height'] = $size[1]; $sub_size_data['filesize'] = $filesize; $sub_size_data['file'] = _wp_relative_upload_path( $path ); } else { // As above, $size was already read for every size reaching here. $sub_size_data['width'] = $size ? $size[0] : 0; $sub_size_data['height'] = $size ? $size[1] : 0; $sub_size_data['file'] = wp_basename( $path ); $sub_size_data['mime_type'] = $type; $sub_size_data['filesize'] = wp_filesize( $path ); } /* * Record the file names produced for this attachment so finalize can * confirm every stored sub-size was actually sideloaded here. The * values recorded are exactly the ones handed back to the client, so * finalize accepts a submission only when it echoes what was produced. */ foreach ( array( 'file', 'original_image' ) as $provenance_key ) { if ( isset( $sub_size_data[ $provenance_key ] ) && is_string( $sub_size_data[ $provenance_key ] ) && '' !== $sub_size_data[ $provenance_key ] ) { add_post_meta( $attachment_id, self::META_KEY_SIDELOAD_FILE_NAME, wp_slash( $sub_size_data[ $provenance_key ] ) ); } } return rest_ensure_response( $sub_size_data ); } /** * Filters wp_unique_filename during sideloads. * * wp_unique_filename() will always add numeric suffix if the name looks like a sub-size to avoid conflicts. * Adding this closure to the filter helps work around this safeguard. * * Example: when uploading myphoto.jpeg, WordPress normally creates myphoto-150x150.jpeg, * and when uploading myphoto-150x150.jpeg, it will be renamed to myphoto-150x150-1.jpeg * However, here it is desired not to add the suffix in order to maintain the same * naming convention as if the file was uploaded regularly. * * The suffix is only dropped when no file of that name already exists in $dir, * so this never returns a name that would overwrite one. The unsuffixed name * must also derive from the attachment's own file name, and * {@see self::sideload_item()} pins the upload to the attachment's own * directory, so any name returned here belongs to the attachment being * extended. * * @since 7.1.0 * * @link https://github.com/WordPress/wordpress-develop/blob/30954f7ac0840cfdad464928021d7f380940c347/src/wp-includes/functions.php#L2576-L2582 * * @param string $filename Unique file name. * @param string $dir Directory path. * @param int|string $number The highest number that was used to make the file name unique * or an empty string if unused. * @param string|null $attachment_filename Original attachment file name. * @return string Filtered file name. */ private static function filter_wp_unique_filename( $filename, $dir, $number, $attachment_filename ) { if ( ! is_int( $number ) || ! $attachment_filename ) { return $filename; } $ext = pathinfo( $filename, PATHINFO_EXTENSION ); $name = pathinfo( $filename, PATHINFO_FILENAME ); $orig_name = pathinfo( $attachment_filename, PATHINFO_FILENAME ); if ( ! $ext || ! $name ) { return $filename; } $matches = array(); if ( preg_match( '/(.*)-(\d+x\d+|scaled)-' . $number . '$/', $name, $matches ) ) { $filename_without_suffix = $matches[1] . '-' . $matches[2] . ".$ext"; if ( $matches[1] === $orig_name && ! file_exists( "$dir/$filename_without_suffix" ) ) { return $filename_without_suffix; } } return $filename; } /** * Validates the `sub_sizes` file names against what this attachment produced. * * The {@see self::finalize_item()} method stores the client-supplied `file` * and `original_image` values in the attachment metadata, where they are * later resolved within the attachment's upload directory and read or deleted * (for example by {@see wp_get_original_image_path()}, {@see wp_getimagesize()}, * and {@see wp_delete_attachment_files()}). * * Every file the sideload endpoint creates is recorded under * {@see self::META_KEY_SIDELOAD_FILE_NAME} as it is produced, using * server-generated names. finalize accepts a `file` or `original_image` * value only when it matches one of those recorded names (or the * attachment's own attached file, which it definitionally owns). * * @since 7.1.0 * * @param int $attachment_id The attachment being finalized. * @param array $sub_sizes Sub-size metadata collected from sideloads. * @return true|WP_Error True if every file name was produced here, WP_Error otherwise. * * @phpstan-param list $sub_sizes */ protected function validate_sub_size_provenance( int $attachment_id, array $sub_sizes ) { $allowed = $this->get_sideloaded_file_names( $attachment_id ); foreach ( $sub_sizes as $sub_size ) { foreach ( array( 'file', 'original_image' ) as $key ) { /* * Every value that was sent is checked, no matter how unlikely * a name it looks. A loose emptiness test would wave through * '0', which is a valid one-character name as far as the schema * is concerned and is stored like any other. A value the schema * types as a string but which arrives as something else is * rejected rather than skipped, so a subclass which widens the * schema cannot pass an unchecked value on to the metadata. */ if ( ! isset( $sub_size[ $key ] ) ) { continue; } if ( ! is_string( $sub_size[ $key ] ) || ! in_array( $sub_size[ $key ], $allowed, true ) ) { return new WP_Error( 'rest_invalid_sub_size_file', __( 'Invalid sub-size file name. File names must have been produced by a prior sideload for this attachment.' ), array( 'status' => 400 ) ); } } } return true; } /** * Returns the file names which a finalize request may store for an attachment. * * The set is the file names the sideload endpoint recorded as it produced * them (ref. {@see self::META_KEY_SIDELOAD_FILE_NAME}), plus the attachment's own * attached file - accepted in both its uploads-relative and basename form so * a scaled main-file pointer validates regardless of which the client * echoes - plus the names already stored in the attachment's own metadata. * * @since 7.1.0 * * @param int $attachment_id The attachment being finalized. * @param bool $include_provenance Whether to include the sideload provenance rows. * Pass false to get only the names recoverable from * the attached file and stored metadata, e.g. to decide * whether a provenance row is still needed. Default true. * @return string[] File names that may appear in the finalize submission. * * @phpstan-return list */ protected function get_sideloaded_file_names( int $attachment_id, bool $include_provenance = true ): array { $allowed = array(); if ( $include_provenance ) { foreach ( (array) get_post_meta( $attachment_id, self::META_KEY_SIDELOAD_FILE_NAME ) as $name ) { if ( is_string( $name ) && '' !== $name ) { $allowed[] = $name; } } } $attached_file = get_post_meta( $attachment_id, '_wp_attached_file', true ); if ( is_string( $attached_file ) && strlen( $attached_file ) > 0 ) { $allowed[] = $attached_file; $allowed[] = wp_basename( $attached_file ); } /* * Names already stored in this attachment's metadata passed this same * check when they were written, so accepting them again introduces * nothing new. */ $metadata = wp_get_attachment_metadata( $attachment_id, true ); if ( is_array( $metadata ) ) { $stored = array( $metadata['file'] ?? null, $metadata['original_image'] ?? null, $metadata[ self::META_KEY_SOURCE_IMAGE ] ?? null, $metadata['animated_video'] ?? null, $metadata['animated_video_poster'] ?? null, ); if ( ! empty( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) { foreach ( $metadata['sizes'] as $size ) { $stored[] = is_array( $size ) ? ( $size['file'] ?? null ) : null; } } foreach ( $stored as $name ) { if ( is_string( $name ) && '' !== $name ) { $allowed[] = $name; $allowed[] = wp_basename( $name ); } } } return array_values( array_unique( $allowed ) ); } /** * Returns the uploads subdirectory an attachment is stored in. * * Used to place a sideloaded file alongside the attachment it extends. The * result is concatenated into a filesystem path by the caller, so it is * returned only when the attachment resolves inside the uploads directory * and the stored path is well formed. * * @since 7.1.0 * * @param string $attached_file Absolute path to the attached file. * @return string|null Subdirectory beginning with a slash, an empty string when the * attachment sits in the base directory, or null when the * attachment is not inside the uploads directory. * * @phpstan-param non-empty-string $attached_file */ protected function get_attachment_upload_subdir( string $attached_file ): ?string { $uploads = wp_get_upload_dir(); if ( empty( $uploads['basedir'] ) ) { return null; } $basedir = untrailingslashit( wp_normalize_path( $uploads['basedir'] ) ); $file_dir = wp_normalize_path( dirname( $attached_file ) ); /* * The attachment's directory must be the uploads base directory itself * or a directory inside it. The trailing slash in the prefix comparison * keeps a sibling directory that merely shares the prefix (for example * 'uploads-elsewhere' next to 'uploads') from matching. */ if ( $file_dir !== $basedir && ! str_starts_with( $file_dir, trailingslashit( $basedir ) ) ) { return null; } $subdir = (string) substr( $file_dir, strlen( $basedir ) ); // A prefix match alone does not rule out a path that climbs back out. if ( in_array( '..', explode( '/', $subdir ), true ) ) { return null; } return $subdir; } /** * Finalizes an attachment after client-side media processing. * * Applies the sub-size metadata collected from sideload responses in a * single metadata update, then triggers the 'wp_generate_attachment_metadata' * filter so that server-side plugins can process the attachment after all * client-side operations (upload, thumbnail generation, sideloads) are * complete. * * @since 7.1.0 * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure. */ public function finalize_item( WP_REST_Request $request ) { $attachment_id = (int) $request['id']; $post = $this->get_post( $attachment_id ); if ( is_wp_error( $post ) ) { return $post; } /** * Sub-size metadata collected from sideload responses. Confirm every * file name was produced by a prior sideload for this attachment before * storing it, so a client cannot make finalize record (and later read or * delete) another attachment's files. * * @var list $sub_sizes */ $sub_sizes = $request['sub_sizes'] ?? array(); $provenance = $this->validate_sub_size_provenance( $attachment_id, $sub_sizes ); if ( is_wp_error( $provenance ) ) { return $provenance; } $metadata = wp_get_attachment_metadata( $attachment_id ); if ( ! is_array( $metadata ) ) { $metadata = array(); } // Apply all sub-size metadata collected from sideload responses. foreach ( $sub_sizes as $sub_size ) { $image_size = $sub_size['image_size']; // When multiple size names share identical dimensions the client // sends a single sub-size entry with an array of names. Register the // same file under each name. if ( is_array( $image_size ) ) { /* * Arrays carry regular sizes only, as the sideload endpoint * enforces. Each special size names a single file handled by one * of the branches below, so grouping one under a shared file * would write it to the wrong place; reject rather than guess. */ if ( array_intersect( $image_size, self::get_special_image_sizes() ) ) { return new WP_Error( 'rest_invalid_sub_size_name', __( 'A grouped sub-size entry may only name regular image sizes.' ), array( 'status' => 400 ) ); } // As below: `file` is not required by the schema, and a size // entry that names no file is not worth recording. if ( empty( $sub_size['file'] ) ) { continue; } $metadata['sizes'] = $metadata['sizes'] ?? array(); foreach ( $image_size as $name ) { $metadata['sizes'][ $name ] = array( 'width' => $sub_size['width'] ?? 0, 'height' => $sub_size['height'] ?? 0, 'file' => $sub_size['file'], 'mime-type' => $sub_size['mime_type'] ?? '', 'filesize' => $sub_size['filesize'] ?? 0, ); } continue; } if ( 'original' === $image_size || 'scaled' === $image_size ) { // Skip malformed entries so a bad payload cannot blank out the // main file metadata. if ( empty( $sub_size['file'] ) ) { continue; } /* * Record the supplied full-size image (from sideload_item()) as * the main file, keeping the current attached file as * `original_image`. A 'scaled' image is downsized and an * 'original' image is rotated; both have any EXIF orientation * already applied by the client. */ if ( ! empty( $sub_size['original_image'] ) ) { $metadata['original_image'] = $sub_size['original_image']; } $metadata['width'] = $sub_size['width'] ?? 0; $metadata['height'] = $sub_size['height'] ?? 0; $metadata['filesize'] = $sub_size['filesize'] ?? 0; $metadata['file'] = $sub_size['file']; /* * The supplied image has its orientation applied already, so * reset the stored value (from the upload) to 1, as * wp_create_image_subsizes() does for both its scale and rotate * paths. Otherwise exif_orientation would still report the * pre-rotation value and the client would rotate the image * again on a re-fetch. */ if ( ! empty( $metadata['image_meta']['orientation'] ) ) { $metadata['image_meta']['orientation'] = 1; } } elseif ( self::IMAGE_SIZE_SOURCE_ORIGINAL === $image_size ) { // As above: `file` is not required by the schema, and each of // these sizes is nothing but the file it names. if ( empty( $sub_size['file'] ) ) { continue; } /* * Source-format original: stored under its own meta key so the * scaled-sideload flow (which writes 'original_image') cannot * clobber it. 'original_image' keeps pointing at the * web-viewable JPEG derivative. Cleanup on attachment delete * is handled by wp_delete_attachment_files(). */ $metadata[ self::META_KEY_SOURCE_IMAGE ] = $sub_size['file']; } elseif ( 'animated_video' === $image_size ) { if ( empty( $sub_size['file'] ) ) { continue; } /* * Converted-video companion of an animated GIF. Stored under its * own meta key; 'original_image' keeps pointing at the GIF. Cleanup * on attachment delete is handled by wp_delete_attachment_files(). */ $metadata['animated_video'] = $sub_size['file']; } elseif ( 'animated_video_poster' === $image_size ) { if ( empty( $sub_size['file'] ) ) { continue; } // Static first-frame poster for the converted video. $metadata['animated_video_poster'] = $sub_size['file']; } else { if ( empty( $sub_size['file'] ) ) { continue; } $metadata['sizes'] = $metadata['sizes'] ?? array(); $metadata['sizes'][ $image_size ] = array( 'width' => $sub_size['width'] ?? 0, 'height' => $sub_size['height'] ?? 0, 'file' => $sub_size['file'], 'mime-type' => $sub_size['mime_type'] ?? '', 'filesize' => $sub_size['filesize'] ?? 0, ); } } /** This filter is documented in wp-admin/includes/image.php */ $metadata = apply_filters( 'wp_generate_attachment_metadata', $metadata, $attachment_id, 'update' ); wp_update_attachment_metadata( $attachment_id, $metadata ); /* * Drop only the provenance rows this request consumed, now that the * names are recorded in the metadata itself. A row is dropped only once * its name is recoverable from the stored metadata, so a name the * 'wp_generate_attachment_metadata' filter removed - or that a failed * update never persisted - keeps its row and the retried request the * endpoint documents as idempotent still validates. Rows for sideloads * that have not been finalized yet survive for a later call, and passing * the value makes the delete a no-op when the row is already gone, so a * retried request cleans up without error. Any rows left behind by an * abandoned upload are removed with the attachment itself. * * Retrying is idempotent for the request as it was sent. A name is only * unavailable to a retry once a later finalize has overwritten the same * size with a newly sideloaded file, which drops the earlier name from * the metadata the retry recovers it from. * * The names are collected before deleting so a request which repeats * the same name across many sub-sizes still issues one query per * distinct name. */ $recoverable = $this->get_sideloaded_file_names( $attachment_id, false ); $consumed = array(); foreach ( $sub_sizes as $sub_size ) { foreach ( array( 'file', 'original_image' ) as $key ) { // Matches the set validate_sub_size_provenance() checked, so // every name a request was allowed to store is also cleaned up. if ( isset( $sub_size[ $key ] ) && is_string( $sub_size[ $key ] ) && in_array( $sub_size[ $key ], $recoverable, true ) ) { $consumed[] = $sub_size[ $key ]; } } } foreach ( array_unique( $consumed ) as $file_name ) { delete_post_meta( $attachment_id, self::META_KEY_SIDELOAD_FILE_NAME, wp_slash( $file_name ) ); } $response_request = new WP_REST_Request( WP_REST_Server::READABLE, rest_get_route_for_post( $attachment_id ) ); $response_request['context'] = 'edit'; if ( isset( $request['_fields'] ) ) { $response_request['_fields'] = $request['_fields']; } return $this->prepare_item_for_response( $post, $response_request ); } } آچار سوکت زن شبکه پروسکیت مدل CP-376TR - ابزارونیک
علاقه مندی
ورود / ثبت نام
  • محصولات
    • ابزار دقیق
      • اسیلوسکوپ
      • USB تستر
      • تستر SMD
      • تستر شبکه و زوج یاب
      • تستر ولتاژ دوشاخ
      • بادسنج
      • دورسنج
      • صوت سنج
      • دماسنج و رطوبت سنج
      • فازیاب
      • متر لیزری
      • کلمپ متر
      • لوکس متر
      • LCR متر
      • مولتی متر
      • وات متر
    • قطعات و تجهیزات الکترونیک
      • آی سی
      • انواع ترانزیستور
      • انواع خازن
      • انواع دیود
      • انواع کانکتور
      • انواع مقاومت
    • ابزارآلات برقی
      • التراسونیک
      • پری هیتر
      • پیچ گوشتی برقی و شارژی
      • تفنگ چسب حرارتی
      • دستگاه پاک کننده چسب
      • دستگاه هیترو هویه
      • سپراتور
      • سشوار صنعتی
      • قلع کش برقی
      • لامپ UV
      • مینی فرز و مینی دریل
      • هویه قلمی
    • ابزار دستی
      • آچار آلن
      • آچار بکس
      • آچار پرس شبکه
      • آچار پرس کانکتور
      • آچار فرانسه
      • آچار پانچ و کروز
      • آی سی کش
      • ابزار لحیم کاری
      • اسپاتول و قاب باز کن
      • انبر دست
      • انبر قفلی
      • انواع گیره
      • پایه هویه
      • پنس
      • پیچ گوشتی
      • تاچ کش
      • جا الکلی
      • خار باز کن
      • دم باریک
      • سوهان
      • سیم چین
      • سیم لخت کن
    • لوپ و ذره‌بین
      • لوپ دو چشمی
      • لوپ سه چشمی
      • لوپ دیجیتال
      • تجهیزات جانبی لوپ
    • سایر
  • دوربین مداربسته
  • تماس با ما
  • درباره ما
0 مقایسه
1 محصول / 980,000 تومان
مارا دنبال کنید
منو
1 محصول / 980,000 تومان
View cart “سیم چین و سیم لخت کن اتوماتیک پروسکیت ProsKit CP-367A” has been added to your cart.
proskit
اچار سوکت زن 376TR
آچار سوکت زن شبکه
بزرگنمایی تصویر
Home ابزار دستی آچار پرس شبکه آچار سوکت زن شبکه پروسکیت مدل CP-376TR
لوپ سه چشمی کایلیوی RF4 RF-7050 TVP 14,200,000 تومان
بازگشت به محصولات
آچار سوکت زن شبکه پروسکیت مدل CP-376M 4,650,000 تومان
proskit

آچار سوکت زن شبکه پروسکیت مدل CP-376TR

1,595,000 تومان

آچار پرس شبکه

مقایسه
افزودن به علاقه مندی
Categories: آچار پرس شبکه, ابزار دستی
Share:
  • Description
  • Additional information
  • Reviews (0)
  • درباره برند
  • خرید و فروش
Description
مناسب برای پرس کردن انواع سوکت تلفن و سوکت های شبکه 4و 6 و 8 پین
ابزار دستی 
Additional information
Brand

proskit

Reviews (0)

Reviews

There are no reviews yet.

Be the first to review “آچار سوکت زن شبکه پروسکیت مدل CP-376TR” لغو پاسخ

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

درباره برند
خرید و فروش

Related products

sunshine-tools
مقایسه

پنس سر صاف سانشاین SUNSHINE SH_11

ابزار دستی, پنس
135,000 تومان
پنس سرصاف سانشاین SUNSHINE SH_11 دارای ویژگی های زیر میباشد: مقاوم در برابر ضربه قابلیت آنتی استاتیک مقاوم در برابر
افزودن به علاقه مندی
Add to cart
مشاهده سریع
proskit
سیم لخت کن اتوماتیک پروسکیت ProsKit CP-367A- ابزارونیک
سیم لخت کن اتوماتیک پروسکیت ProsKit CP-367A- ابزارونیک
مقایسه

سیم چین و سیم لخت کن اتوماتیک پروسکیت ProsKit CP-367A

ابزار دستی, سیم لخت کن
980,000 تومان
سیم لخت کن یونیورسال پروسکیت
افزودن به علاقه مندی
Add to cart
مشاهده سریع
proskit
آچار پرس BNC پروسکیت 230PA
مقایسه

آچار پرس BNC پروسکیت تایوانی Proskit مدل ۶PK-230PA

ابزار دستی, آچار پرس کانکتور
2,500,000 تومان
آچار پرس BNC برند پروسکیت 6pk-230PA 1.7 2.5 5.4 6.48 8.2 HEX Size : mm مجهز به قفل جغجغه ای
افزودن به علاقه مندی
Add to cart
مشاهده سریع
sunshine-tools
مقایسه

پنس سرکج سانشاین SUNSHINE Sk_15

ابزار دستی, پنس
220,000 تومان
پنس سرکج سانشاین SUMSHINE SK_15، ابزاریست ساخته شده با دستگاه های دقیق که: پنسی با کیفیت بالاست مناسب تعمیرات موبایل، و
افزودن به علاقه مندی
Add to cart
مشاهده سریع
sunshine-tools
مقایسه

پنس سرکج سانشاینSUNSHINE SH-15

ابزار دستی, پنس
135,000 تومان
پنس سرکج سانشاین SUNSHINE SH_15 دارای ویژگی های زیر میباشد: ساخته شده از فولاد ضد زنگ، ضد اکسید، و اکسیداسیون
افزودن به علاقه مندی
Add to cart
مشاهده سریع
proskit
انبر سیم لخت کن پروسکیت تایوان مدل Proskit 6PK-223
مقایسه

سیم لخت کن پروسکیت مدل Pro’sKit 6PK-223

ابزار دستی, آچار پرس کانکتور
535,000 تومان
  • مناسب برای سیم های 0.5mm ~ 4mm
    • (AWG: 10 ~ 30)
  • دارای سیم چین
  • جنس بدنه S45C
  • جنس روکش PVC
  • سختی  HRC 45˚±3
  • طول ابزار 127mm
  • از مزایای این سیم لخت کن می‌توان قابلیت تنظیم برای سیم های 0.5mm ~ 4mm و سیم چین اشاره کرد.
افزودن به علاقه مندی
Add to cart
مشاهده سریع
proskit
سیم لخت کن پروسکیت مدل Proskit 8PK-3161
مقایسه

انبر سیم لخت کن پروسکیت تایوان مدل Proskit 8PK-3161

ابزار دستی, سیم لخت کن
660,000 تومان
انبر سیم لخت کن پروسکیت تایوان مدل Proskit 8PK-3161 سایز سیم لخت کن 0.9 ، 1.25 ، 2.0 ، 3.5 و 5.5 میلی متر
حداکثر میزان باز شدن 30 میلی‌متر
ساخت تایوان
افزودن به علاقه مندی
Add to cart
مشاهده سریع
proskit
سیم لخت کن پروسکیت مدل 369A
مقایسه

سیم لخت کن اتوماتیک پروسکیت مدل ProsKit 608-369A

ابزار دستی, آچار پرس کانکتور
1,100,000 تومان
  • مناسب برای سیم ها با قطر 0.5 , 1.2 , 1.6 , 2.0 میلی متر
  • کیفیت تولید خوب ، دقت مناسب و دوام بالا
  • جنس بدنهZinc alloy با روکش PVC
  • جنس تیغه SS41
  • جنس روکش ABS + TPR
  • سختی بدنه HV 450~800
  • سختی تیغه  44˚~55˚ HRC
  • طول ابزار 170mm
افزودن به علاقه مندی
Add to cart
مشاهده سریع
DAHUA
FLUKE
ANALOG-DVICES
RELIFE
GOOT
yaxun
quick
SUNSHINE-TOOLS
PROSKIT
MINI-CIRCUITS
UNIT
Hioki
HIKVISION
RIGOL
logo12

ابزارونیک مرجع ابزارآلات الکترونیکی

  • ایران تهران خیابان جمهوری
  • 021-66751084
  • info@abzaronic.com
آخرین مقالات
  • ایران وود مارت بهترین انتخاب
  • electronics-2-blog-7
    بهترین انتخاب در کامپیوتر و لپ تاب
  • دکوراسیون داخلی خونه
  • میز های ایرانی در برند مختلف
پر امتیازترین محصولات
  • 7071 تستر کابل شبکه دیجیتال پروسکیت مدل ProsKit MT-7071 14,375,000 تومان
  • Placeholder آمپرمتر کلمپی ولتاژ بالا دیحیتال یونیتی UNI_T UT255A
  • Placeholder ترمومتر لیزری تفنگی 1300 درجه یونیتی +UNI_T UT-303C
لینک های مفید
  • قوانین و مقررات
  • سوالات متداول
  • خبرنامه
  • تماس با ما
  • اخبار جدید
  • خرید های قدیمی
منوی فوتر
  • اینستاگرام
  • تماس با ما
محتوای این سایت شخصی می‌باشد.
  • منو
  • دسته بندی ها
  • محصولات
    • ابزار دقیق
      • اسیلوسکوپ
      • USB تستر
      • تستر SMD
      • تستر شبکه و زوج یاب
      • تستر ولتاژ دوشاخ
      • بادسنج
      • دورسنج
      • صوت سنج
      • دماسنج و رطوبت سنج
      • فازیاب
      • متر لیزری
      • کلمپ متر
      • لوکس متر
      • LCR متر
      • مولتی متر
      • وات متر
    • قطعات و تجهیزات الکترونیک
      • آی سی
      • انواع ترانزیستور
      • انواع خازن
      • انواع دیود
      • انواع کانکتور
      • انواع مقاومت
    • ابزارآلات برقی
      • التراسونیک
      • پری هیتر
      • پیچ گوشتی برقی و شارژی
      • تفنگ چسب حرارتی
      • دستگاه پاک کننده چسب
      • دستگاه هیترو هویه
      • سپراتور
      • سشوار صنعتی
      • قلع کش برقی
      • لامپ UV
      • مینی فرز و مینی دریل
      • هویه قلمی
    • ابزار دستی
      • آچار آلن
      • آچار بکس
      • آچار پرس شبکه
      • آچار پرس کانکتور
      • آچار فرانسه
      • آچار پانچ و کروز
      • آی سی کش
      • ابزار لحیم کاری
      • اسپاتول و قاب باز کن
      • انبر دست
      • انبر قفلی
      • انواع گیره
      • پایه هویه
      • پنس
      • پیچ گوشتی
      • تاچ کش
      • جا الکلی
      • خار باز کن
      • دم باریک
      • سوهان
      • سیم چین
      • سیم لخت کن
    • لوپ و ذره‌بین
      • لوپ دو چشمی
      • لوپ سه چشمی
      • لوپ دیجیتال
      • تجهیزات جانبی لوپ
    • سایر
  • دوربین مداربسته
  • تماس با ما
  • درباره ما
  • محصولات
    • ابزار دقیق
      • اسیلوسکوپ
      • USB تستر
      • تستر SMD
      • تستر شبکه و زوج یاب
      • تستر ولتاژ دوشاخ
      • بادسنج
      • دورسنج
      • صوت سنج
      • دماسنج و رطوبت سنج
      • فازیاب
      • متر لیزری
      • کلمپ متر
      • لوکس متر
      • LCR متر
      • مولتی متر
      • وات متر
    • قطعات و تجهیزات الکترونیک
      • آی سی
      • انواع ترانزیستور
      • انواع خازن
      • انواع دیود
      • انواع کانکتور
      • انواع مقاومت
    • ابزارآلات برقی
      • التراسونیک
      • پری هیتر
      • پیچ گوشتی برقی و شارژی
      • تفنگ چسب حرارتی
      • دستگاه پاک کننده چسب
      • دستگاه هیترو هویه
      • سپراتور
      • سشوار صنعتی
      • قلع کش برقی
      • لامپ UV
      • مینی فرز و مینی دریل
      • هویه قلمی
    • ابزار دستی
      • آچار آلن
      • آچار بکس
      • آچار پرس شبکه
      • آچار پرس کانکتور
      • آچار فرانسه
      • آچار پانچ و کروز
      • آی سی کش
      • ابزار لحیم کاری
      • اسپاتول و قاب باز کن
      • انبر دست
      • انبر قفلی
      • انواع گیره
      • پایه هویه
      • پنس
      • پیچ گوشتی
      • تاچ کش
      • جا الکلی
      • خار باز کن
      • دم باریک
      • سوهان
      • سیم چین
      • سیم لخت کن
    • لوپ و ذره‌بین
      • لوپ دو چشمی
      • لوپ سه چشمی
      • لوپ دیجیتال
      • تجهیزات جانبی لوپ
    • سایر
  • دوربین مداربسته
  • تماس با ما
  • درباره ما
  • علاقه مندی
  • مقایسه
  • ورود / ثبت نام
سبد خرید
بستن (Esc)

ورود

بستن (Esc)

رمز عبور را فراموش کرده اید؟

هنوز حساب کاربری ندارید؟

ایجاد حساب کاربری
برای دیدن محصولات که دنبال آن هستید تایپ کنید.