ImageCopier::copyToRemoteSite()   B
last analyzed

Complexity

Conditions 10
Paths 7

Size

Total Lines 38
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 29
dl 0
loc 38
rs 7.6666
c 1
b 0
f 0
cc 10
nc 7
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace GeminiLabs\SiteReviews\Integrations\MultilingualPress;
4
5
use GeminiLabs\SiteReviews\Database;
6
use GeminiLabs\SiteReviews\Database\Query;
7
use GeminiLabs\SiteReviews\Helpers\Cast;
8
use GeminiLabs\SiteReviews\Helpers\Str;
9
use Inpsyde\MultilingualPress\Editor\Notices\ExistingAttachmentsNotice;
10
use Inpsyde\MultilingualPress\Framework\Filesystem;
11
use Inpsyde\MultilingualPress\Framework\SwitchSiteTrait;
12
use Inpsyde\MultilingualPress\TranslationUi\Post\RelationshipContext;
13
14
class ImageCopier
15
{
16
    use SwitchSiteTrait;
17
18
    protected RelationshipContext $context;
19
    protected ExistingAttachmentsNotice $existingAttachmentsNotice;
20
    protected Filesystem $filesystem;
21
22
    public function __construct(
23
        Filesystem $filesystem,
24
        ExistingAttachmentsNotice $existingAttachmentsNotice
25
    ) {
26
        $this->existingAttachmentsNotice = $existingAttachmentsNotice;
27
        $this->filesystem = $filesystem;
28
    }
29
30
    /**
31
     * Retrieve the attachment id of the existing attachment based on the file name.
32
     */
33
    public function attachmentIdFromPath(string $attachmentPath): int
34
    {
35
        return $this->existingAttachmentId($attachmentPath);
36
    }
37
38
    /**
39
     * Copy attachments from source site to the give remote site using a list of attachment ids.
40
     */
41
    public function copyById(int $sourceSiteId, int $remoteSiteId, array $sourceAttachmentIds): array
42
    {
43
        $this->context = new RelationshipContext([
44
            RelationshipContext::REMOTE_SITE_ID => $remoteSiteId,
45
            RelationshipContext::SOURCE_SITE_ID => $sourceSiteId,
46
        ]);
47
        return $this->copyAttachmentsFromSource($sourceAttachmentIds);
48
    }
49
50
    /**
51
     * Copy attachments from source site to the give remote site using a list of attachment ids.
52
     */
53
    public function copyByIdWithContext(RelationshipContext $context, array $sourceAttachmentIds): array
54
    {
55
        $this->context = $context;
56
        return $this->copyAttachmentsFromSource($sourceAttachmentIds);
57
    }
58
59
    /**
60
     * Retrieve the meta by the given attachment post.
61
     */
62
    protected function attachmentMeta(\WP_Post $attachment): array
63
    {
64
        $altMeta = (string) get_post_meta($attachment->ID, '_wp_attachment_image_alt', true);
65
        $sourceUrl = (string) get_post_meta($attachment->ID, '_source_url', true);
66
        return array_filter([
67
            '_source_url' => sanitize_url($sourceUrl), // this will exist if the image was imported from a CSV file
68
            '_wp_attachment_image_alt' => sanitize_text_field($altMeta),
69
        ]);
70
    }
71
72
    protected function copyAttachmentIds(array $sourceAttachmentIds): array
73
    {
74
        $sourceAttachmentIds = $this->ensureAttachmentIds($sourceAttachmentIds);
75
        $sourceAttachments = $this->sourceAttachments($sourceAttachmentIds);
76
        return $sourceAttachments
77
            ? $this->copyToRemoteSite(...$sourceAttachments)
78
            : [];
79
    }
80
81
    protected function copyAttachmentsFromSource(array $sourceAttachmentIds): array
82
    {
83
        $attachmentIds = [];
84
        $reviewAttachmentIds = [];
85
        if (class_exists('GeminiLabs\SiteReviews\Addon\Images\Uploader')) {
86
            $reviewAttachmentIds = array_filter($sourceAttachmentIds,
87
                fn ($attachmentId) => str_contains((string) get_attached_file($attachmentId, true), 'site-reviews/')
88
            );
89
            if (!empty($reviewAttachmentIds)) {
90
                glsr('Addon\Images\Uploader')->setUploadPath(glsr()->id);
91
                glsr('Addon\Images\Uploader')->setIntermediateImageSizes();
92
                $attachmentIds = $this->copyAttachmentIds($reviewAttachmentIds);
93
                glsr('Addon\Images\Uploader')->resetIntermediateImageSizes();
94
                glsr('Addon\Images\Uploader')->resetUploadPath();
95
            }
96
        }
97
        if ($otherAttachmentIds = array_diff($sourceAttachmentIds, $reviewAttachmentIds)) {
98
            $copiedAttachmentIds = $this->copyAttachmentIds($otherAttachmentIds);
99
            return array_merge($attachmentIds, $copiedAttachmentIds);
100
        }
101
        return $attachmentIds;
102
    }
103
104
    /**
105
     * Copy the attachment meta from the source give post to the remote attachment.
106
     */
107
    protected function copyMetaFromSourceAttachment(
108
        AttachmentData $sourceAttachmentData,
109
        int $remoteAttachmentId
110
    ): void {
111
        foreach ($sourceAttachmentData->meta() as $attachmentKey => $sourceAttachmentMeta) {
112
            update_post_meta($remoteAttachmentId, $attachmentKey, $sourceAttachmentMeta);
113
        }
114
    }
115
116
    /**
117
     * Copy the attachment sizes from the given source post to the remote site.
118
     */
119
    protected function copySizesFromSourceAttachment(
120
        AttachmentData $sourceAttachmentData,
121
        int $remoteAttachmentId
122
    ): void {
123
        $remoteUploadPath = wp_upload_dir()['path'] ?? '';
124
        $sourceUploadPath = dirname($sourceAttachmentData->filePath());
125
        $sourceAttachmentBasename = wp_basename($sourceAttachmentData->filePath());
126
        $remoteAttachmentRealPath = "{$remoteUploadPath}/{$sourceAttachmentBasename}";
127
        $metadata = $sourceAttachmentData->fileMeta();
128
        $sizes = wp_get_registered_image_subsizes();
129
        $sizes = apply_filters('intermediate_image_sizes_advanced', $sizes, $metadata, $remoteAttachmentId);
130
        $copiedSizes = [];
131
        foreach (($metadata['sizes'] ?? []) as $size => $meta) {
132
            if (!array_key_exists($size, $sizes)) {
133
                continue;
134
            }
135
            $sourceFile = "{$sourceUploadPath}/{$meta['file']}";
136
            $remoteFile = "{$remoteUploadPath}/{$meta['file']}";
137
            if ($this->filesystem->pathExists($remoteFile) && $this->filesystem->isReadable($remoteFile)) {
138
                $copiedSizes[$size] = $meta;
139
                continue;
140
            }
141
            if ($this->filesystem->copy($sourceFile, $remoteFile)) {
142
                $copiedSizes[$size] = $meta;
143
                continue;
144
            }
145
            $meta = image_make_intermediate_size(
146
                $remoteAttachmentRealPath,
147
                $sizes[$size]['width'] ?? null,
148
                $sizes[$size]['height'] ?? null,
149
                $sizes[$size]['crop'] ?? false
150
            );
151
            if ($meta) {
152
                $copiedSizes[$size] = $meta;
153
            }
154
        }
155
        $metadata['sizes'] = $copiedSizes;
156
        if (empty($metadata['sizes'])) {
157
            $metadata = wp_generate_attachment_metadata($remoteAttachmentId, $remoteAttachmentRealPath);
158
        }
159
        wp_update_attachment_metadata($remoteAttachmentId, $metadata);
160
    }
161
162
    /**
163
     * Copy attachment file to the remote upload dir and create a new attachment post.
164
     */
165
    protected function copyToRemoteSite(AttachmentData ...$sourceAttachmentsData): array
166
    {
167
        $originalSite = $this->maybeSwitchSite($this->context->remoteSiteId());
168
        $remoteAttachments = [];
169
        $uploadDir = wp_upload_dir();
170
        $uploadPath = $uploadDir['path'] ?? '';
171
        $uploadUrl = $uploadDir['url'] ?? '';
172
        if (!$uploadPath || !$uploadUrl || !$this->filesystem->mkDirP($uploadDir['path'])) {
173
            $this->maybeRestoreSite($originalSite);
174
            return [];
175
        }
176
        foreach ($sourceAttachmentsData as $sourceAttachmentData) {
177
            $sourceAttachmentPath = $sourceAttachmentData->filePath();
178
            $sourceAttachmentBasename = wp_basename($sourceAttachmentPath);
179
            if ($existingAttachmentId = $this->existingAttachmentId($sourceAttachmentData->relativePath())) {
180
                $updatedId = $this->updateAttachment($sourceAttachmentData, $existingAttachmentId);
181
                if ($updatedId) {
182
                    $remoteAttachments[] = $updatedId;
183
                }
184
                continue;
185
            }
186
            $remoteAttachmentRealPath = "{$uploadPath}/{$sourceAttachmentBasename}";
187
            $remoteAttachmentUrl = "{$uploadUrl}/{$sourceAttachmentBasename}";
188
            if (!$this->filesystem->pathExists($sourceAttachmentPath)
189
                || !$this->filesystem->isReadable($sourceAttachmentPath)) {
190
                continue;
191
            }
192
            if (!$this->filesystem->copy($sourceAttachmentPath, $remoteAttachmentRealPath)) {
193
                continue;
194
            }
195
            $remoteAttachments[] = $this->createAttachmentPostByPath(
196
                $sourceAttachmentData,
197
                $remoteAttachmentRealPath,
198
                $remoteAttachmentUrl
199
            );
200
        }
201
        $this->maybeRestoreSite($originalSite);
202
        return array_filter($remoteAttachments);
203
    }
204
205
    /**
206
     * Create an attachment post by the attachment path.
207
     */
208
    protected function createAttachmentPostByPath(
209
        AttachmentData $sourceAttachmentData,
210
        string $remoteAttachmentRealPath,
211
        string $remoteAttachmentUrl
212
    ): int {
213
        $filetype = wp_check_filetype($remoteAttachmentRealPath);
214
        $sourceAttachment = $sourceAttachmentData->post();
215
        $remoteAttachmentData = [
216
            'guid' => $remoteAttachmentUrl,
217
            'menu_order' => $sourceAttachment->menu_order,
218
            'post_author' => get_current_user_id(),
219
            'post_content' => $sourceAttachment->post_content,
220
            'post_excerpt' => $sourceAttachment->post_excerpt,
221
            'post_mime_type' => $filetype['type'] ?? '',
222
            'post_title' => $sourceAttachment->post_title,
223
        ];
224
        $this->requireAttachmentFunctions();
225
        $remoteAttachmentId = wp_insert_attachment(
226
            $remoteAttachmentData,
227
            $remoteAttachmentRealPath,
228
            $this->context->remotePostId(), // attach to the remote post
229
        );
230
        if ($remoteAttachmentId instanceof \WP_Error) {
231
            return 0;
232
        }
233
        $this->copyMetaFromSourceAttachment(
234
            $sourceAttachmentData,
235
            $remoteAttachmentId
236
        );
237
        $this->copySizesFromSourceAttachment(
238
            $sourceAttachmentData,
239
            $remoteAttachmentId
240
        );
241
        return $remoteAttachmentId;
242
    }
243
244
    /**
245
     * Ensure attachment ids are valid integer values.
246
     */
247
    protected function ensureAttachmentIds(array $attachmentIds): array
248
    {
249
        return array_filter(wp_parse_id_list($attachmentIds));
250
    }
251
252
    /**
253
     * Retrieve the attachment id of the existing attachment based on the file name.
254
     */
255
    protected function existingAttachmentId(string $attachmentPath): int
256
    {
257
        $relativeAttachmentPath = Str::removePrefix(
258
            $attachmentPath,
259
            trailingslashit(wp_upload_dir()['basedir'] ?? '')
260
        );
261
        $sql = glsr(Query::class)->sql("
262
            SELECT post_id
263
            FROM table|postmeta
264
            WHERE meta_key = '_wp_attached_file'
265
            AND meta_value LIKE %s
266
        ", "%{$relativeAttachmentPath}");
267
        return Cast::toInt(glsr(Database::class)->dbGetVar($sql));
268
    }
269
270
    /**
271
     * Check if an attachment post is a valid attachment.
272
     */
273
    protected function isLocalAttachment(\WP_Post $attachment): bool
274
    {
275
        return is_local_attachment((string) get_permalink($attachment));
276
    }
277
278
    /**
279
     * Require functions to work with attachments.
280
     */
281
    protected function requireAttachmentFunctions(): void
282
    {
283
        if (!\function_exists('wp_generate_attachment_metadata')
284
            && !\function_exists('MultilingualPress\Vendor\wp_generate_attachment_metadata')) {
285
            require_once ABSPATH.'wp-admin/includes/image.php';
286
        }
287
        if (!\function_exists('get_intermediate_image_sizes')) {
288
            require_once ABSPATH.'wp-admin/includes/media.php';
289
        }
290
    }
291
292
    /**
293
     * Retrieve the attachments post and files path
294
     * The items contains the post and the real path of the attachment.
295
     *
296
     * @param int[] $attachmentIds
297
     *
298
     * @return AttachmentData[]
299
     */
300
    protected function sourceAttachments(array $attachmentIds): array
301
    {
302
        $sourceAttachments = [];
303
        foreach ($attachmentIds as $attachmentId) {
304
            $attachment = get_post($attachmentId);
305
            if (!$attachment instanceof \WP_Post || !$this->isLocalAttachment($attachment)) {
306
                continue;
307
            }
308
            $attachmentPath = get_attached_file($attachmentId, true); // unfiltered
309
            if (!$attachmentPath) {
310
                continue;
311
            }
312
            $relativeAttachmentPath = Str::removePrefix(
313
                $attachmentPath,
314
                trailingslashit(wp_upload_dir()['basedir'] ?? '')
315
            );
316
            $sourceAttachments[$attachmentId] = new AttachmentData(
317
                $attachment,
318
                $this->attachmentMeta($attachment),
319
                $attachmentPath,
320
                $relativeAttachmentPath
321
            );
322
        }
323
        return $sourceAttachments;
324
    }
325
326
    /**
327
     * Update the remote attachment post meta data with data provided by the given source attachment.
328
     */
329
    protected function updateAttachment(
330
        AttachmentData $sourceAttachmentData,
331
        int $remoteAttachmentId
332
    ): int {
333
        $data = $sourceAttachmentData->post()->to_array();
334
        $data['ID'] = $remoteAttachmentId;
335
        $data['post_parent'] = $this->context->remotePostId(); // attach to the remote post
336
        $attachmentId = wp_update_post($data, true);
337
        if (is_wp_error($attachmentId)) {
338
            $this->existingAttachmentsNotice->addAttachment(
339
                $sourceAttachmentData->post()->ID,
340
                $this->context->remoteSiteId()
341
            );
342
            return 0;
343
        }
344
        $this->copyMetaFromSourceAttachment(
345
            $sourceAttachmentData,
346
            $remoteAttachmentId
347
        );
348
        $this->copySizesFromSourceAttachment(
349
            $sourceAttachmentData,
350
            $remoteAttachmentId
351
        );
352
        return $attachmentId;
353
    }
354
}
355