PluginProbe
WPOSS阿里云对象存储 / 4.9
WPOSS阿里云对象存储 v4.9
trunk 4.9 5.0
wposs / index.php

index.php in WPOSS阿里云对象存储 4.9, at index.php

516 lines 23.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 Plugin Name: WPOSS(阿里云对象存储)
4 Plugin URI: https://www.laojiang.me/5946.html
5 Description: WordPress同步附件�
6 容远程至阿里云OSS对象存储中,实现网站数据与静态资源分离,提高网站加载速度。微信�
7 �众号: <font color="red">老蒋朋友圈</font>
8 Version: 4.9
9 Author: 老蒋和他的小伙伴
10 Author URI: https://www.laojiang.me
11 */
12 if (!defined('ABSPATH')) die();
13
14 use WPOSS\Api;
15
16 if (!class_exists('WPOSS')) {
17 class WPOSS {
18
19 private $option_name = 'wposs_options'; // 插件参数保存名称
20 private $menu_title = 'WPOSS设置'; // 设置菜单的菜单名
21 private $page_title = 'WPOSS设置'; // 设置菜单的页面title
22 private $capability = 'manage_options'; // 设置页面管理所需权限
23 private $version = '4.9'; // 插件数据版本, 每次修改应与上方的Version值相同
24 private $setting_notices = [
25 'update_success' => '设置已保存', // post数据保存成功时提示�
26
27 'update_failed' => '插件设置更新失败', // 失败时提示
28 ];
29 private $image_display_default_value = 'image/auto-orient,1/quality,q_90/format,webp'; // 数据万象默认规则
30 private $image_display_default_tab = '?x-oss-process='; // 万象规则url连字符
31
32 private $base_folder;
33 private $wp_upload_dir;
34 private $object_storage;
35 private $options;
36
37 function __construct() {
38 # 插件 activation 函数当一个插件在 WordPress 中”activated(启用)”时被触发。
39 register_activation_hook(__FILE__, array($this, 'init_options'));
40 register_deactivation_hook(__FILE__, array($this, 'restore_options')); # 禁用时触发钩子
41
42 $this->includes();
43 $this->constants();
44
45 # 避�
46 �上传插件/主题被同步到对象存储
47 if (substr_count($_SERVER['REQUEST_URI'], '/update.php') <= 0) {
48 add_filter('wp_handle_upload', array($this, 'upload_attachments'));
49 if ( version_compare(get_bloginfo('version'), 5.3, '<') ){
50 add_filter( 'wp_update_attachment_metadata', array($this, 'upload_and_thumbs') );
51 } else {
52 add_filter( 'wp_generate_attachment_metadata', array($this, 'upload_and_thumbs') );
53 add_filter( 'wp_save_image_editor_file', array($this, 'save_image_editor_file') );
54 }
55 }
56
57 if ($this->object_storage->is_client()) {
58 # 检测不重复的文件名
59 add_filter('wp_unique_filename', array($this, 'unique_filename') );
60 }
61
62 # 删除文件时触发删除远端文件,该删除会默认删除缩略图
63 add_action('delete_attachment', array($this, 'delete_remote_attachment'));
64
65 # 添加插件设置菜单
66 add_action('admin_menu', array($this, 'admin_menu_setting'));
67 add_filter('plugin_action_links', array($this, 'setting_plugin_action_links'), 10, 2);
68 # 自动重命名
69 add_filter( 'sanitize_file_name', array($this, 'sanitize_file_name_handler'), 10, 1 );
70 # 图片显示处理
71 add_filter( 'the_content', array($this, 'image_display_processing') );
72 }
73
74 private function includes() {
75 require_once('api.php');
76 }
77
78 private function constants() {
79 $this->base_folder = plugin_basename(dirname(__FILE__));
80 $this->wp_upload_dir = wp_get_upload_dir();
81 $this->options = get_option($this->option_name);
82 # PHP7.4 版本后,对于bool值作为array调用时,会产生警告�
83 容。
84 if (!is_array($this->options)) {
85 $this->init_options();
86 }
87 $this->object_storage = new Api($this->options); // option更新后,若变动了参数,则Api实例的重新创建,目前只有setting中会触发
88 }
89
90 /**
91 * 文件上传功能基础函数,被�
92 �它需要进行文件上传的模块调用
93 * @param $key : 远端需要的Key值[�
94 含路径]
95 * @param $file_local_path : 文件在本地的路径。
96 *
97 * @return bool : 暂未想好如何与wp进行响应。
98
99 */
100 public function _file_upload($key, $file_local_path) {
101 ### 上传文件
102 # 由于增加了独立文件名钩子对cos中同名文件的判断,避�
103 �同名文件的存在,因此这里直接覆盖上传。
104 try {
105 $this->object_storage->Upload(
106 $this->key_handler($key, get_option('upload_url_path')),
107 $file_local_path
108 );
109 // 如果上传成功,且不再本地保存,在此删除本地文件
110 if ($this->options['no_local_file']) {
111 $this->delete_local_file($file_local_path);
112 }
113 return True;
114 } catch (\Exception $e) {
115 return False;
116 }
117 }
118
119 private function remote_key_exist( $filename ) {
120 return $this->object_storage->hasExist( $this->key_handler($this->wp_upload_dir['subdir'] . "/$filename",
121 get_option('upload_url_path')));
122 }
123
124 /**
125 * 删除远程附件(�
126 括图片的原图)
127 * 这里�
128 �部以非/开头,因此上传的函数中也要替换掉key中开头的/
129 * @param $post_id
130 */
131 public function delete_remote_attachment($post_id) {
132 // 获取要删除的对象Key的数组
133 $deleteObjects = array();
134 $meta = wp_get_attachment_metadata( $post_id );
135 $upload_url_path = get_option('upload_url_path');
136
137 if (isset($meta['file'])) {
138 $attachment_key = $meta['file'];
139 array_push($deleteObjects, $this->key_handler($attachment_key, $upload_url_path));
140 } else {
141 $file = get_attached_file( $post_id );
142 $attached_key = str_replace( $this->wp_upload_dir['basedir'] . '/', '', $file ); # 不能以/开头
143 $deleteObjects[] = $this->key_handler($attached_key, $upload_url_path);
144 }
145
146 if (isset($meta['sizes']) && count($meta['sizes']) > 0) {
147 foreach ($meta['sizes'] as $val) {
148 $attachment_thumbs_key = dirname($meta['file']) . '/' . $val['file'];
149 $deleteObjects[] = $this->key_handler($attachment_thumbs_key, $upload_url_path);
150 }
151 }
152
153 if ( !empty( $deleteObjects ) ) {
154 // 执行删除远程对象
155 $allKeys = array_chunk($deleteObjects, 1000); # 每次最多删除1000个,多于1000循环进行
156 foreach ($allKeys as $keys){
157 //删除文件, 每个数组1000个�
158 �素
159 $this->object_storage->Delete($keys);
160 }
161 }
162 }
163
164 // 初始化选项
165 // TODO: 让不同对象存储适用相同参数与setting
166 public function init_options() {
167 $options = array(
168 'version' => $this->version, # 用于以后当有数据结构升级时初始化数据
169 'bucket' => "",
170 'endpoint' => "",
171 'accessKeyId' => "",
172 'accessKeySecret' => "",
173 'no_local_file' => False, # 不在本地保留备份
174 'backup_url_path' => '',
175 'cname' => False, # true为开启CNAME。CNAME是指将自定义域名绑定到存储空间上。可以用来代替ENDPOINT
176 'upload_information' => array(
177 'original' => array(
178 'upload_path' => '',
179 'upload_url_path' => '',
180 ),
181 'active' => array(
182 'upload_path' => '',
183 'upload_url_path' => '',
184 ),
185 ),
186 'opt' => array(
187 'auto_rename' => False,
188 'img_process' => array(
189 'switch' => False,
190 'style_value' => '',
191 ),
192 ),
193 );
194
195 if(!$this->options){
196 if (add_option($this->option_name, $options, '', 'yes')) {
197 $this->options = get_option($this->option_name);
198 }
199 }
200
201 if ( isset($this->options['backup_url_path']) && $this->options['backup_url_path'] != '' ) {
202 update_option('upload_url_path', $this->options['backup_url_path']);
203 // 理论上来说,更新完upload_url_path后,这里的option的backup_url_path还需要修改为'';
204 // 但因为时机上目前只有激活与禁用2种,因此就由禁用时直接赋值,这里减少一次更新。
205 // 后续出现多种场景判断再考虑。
206 }
207 }
208
209 public function restore_options () {
210 $this->options['backup_url_path'] = get_option('upload_url_path');
211 if (update_option($this->option_name, $this->options)) { // 此处修改的参数不影响对象存储实例
212 $this->options = get_option($this->option_name); // 上面的赋值及更新,这里似乎不用再重新获取。 - -!
213 }
214 update_option('upload_url_path', '');
215 }
216
217 /**
218 * 此函数处理上传的key,用于支持 对象存储子目录
219 * @param $key
220 * @param $upload_url_path
221 * @return string
222 */
223 private function key_handler($key, $upload_url_path){
224 # 参数2 为了减少option的获取次数
225 $url_parse = wp_parse_url($upload_url_path);
226 # 约定url不要以/结尾,减少判断条件
227 if (array_key_exists('path', $url_parse)) {
228 if ( substr($key, 0, 1) == '/' ) {
229 $key = $url_parse['path'] . $key;
230 } else {
231 $key = $url_parse['path'] . '/' . $key;
232 }
233 }
234 # $url_parse['path'] 以/开头,在七牛环境下不能以/开头,所以需要处理掉
235 return ltrim($key, '/');
236 }
237
238 /**
239 * 删除本地文件
240 * @param $file_path : 文件路径
241 * @return bool
242 */
243 public function delete_local_file($file_path) {
244 try {
245 if (!@file_exists($file_path)) { # 文件不存在
246 return TRUE;
247 }
248 if (!@unlink($file_path)) { # 删除文件
249 return FALSE;
250 }
251 return TRUE;
252 } catch (Exception $ex) {
253 return FALSE;
254 }
255 }
256
257 /**
258 * 上传图片及缩略图
259 * @param $metadata: 附件�
260 �数据
261 * @return array $metadata: 附件�
262 �数据
263 * 官方的钩子文档上写了可以添加 $attachment_id 参数,但实�
264 测试过程中部分wp接收到不存在的参数时会报错,上传失败,返回报错为“HTTP错误”
265 */
266 public function upload_and_thumbs( $metadata ) {
267 if (isset( $metadata['file'] )) {
268 # 1.�
269 �上传主图
270 $attachment_key = $metadata['file']; // 远程key路径, 此路径不是以/开头
271 $attachment_local_path = $this->wp_upload_dir['basedir'] . '/' . $attachment_key; # 在本地的存储路径
272 $this->_file_upload($attachment_key, $attachment_local_path); # 调用上传函数
273 }
274
275 # 如果存在缩略图则上传缩略图
276 if (isset($metadata['sizes']) && count($metadata['sizes']) > 0) {
277 foreach ($metadata['sizes'] as $val) {
278 $attachment_thumbs_key = dirname($metadata['file']) . '/' . $val['file']; // 生成object 的 key
279 $attachment_thumbs_local_path = $this->wp_upload_dir['basedir'] . '/' . $attachment_thumbs_key; // 本地存储路径
280 $this->_file_upload($attachment_thumbs_key, $attachment_thumbs_local_path); //调用上传函数
281 }
282 }
283
284 return $metadata;
285 }
286
287 /**
288 * @param array $upload {
289 * Array of upload data.
290 *
291 * @type string $file Filename of the newly-uploaded file.
292 * @type string $url URL of the uploaded file.
293 * @type string $type File type.
294 * @return array $upload
295 */
296 public function upload_attachments ($upload) {
297 $mime_types = get_allowed_mime_types();
298 $image_mime_types = array(
299 // Image formats.
300 $mime_types['jpg|jpeg|jpe'],
301 $mime_types['gif'],
302 $mime_types['png'],
303 $mime_types['bmp'],
304 $mime_types['tiff|tif'],
305 $mime_types['ico'],
306 );
307 if ( ! in_array( $upload['type'], $image_mime_types ) ) {
308 $key = str_replace( $this->wp_upload_dir['basedir'] . '/', '', $upload['file'] );
309 $local_path = $upload['file'];
310 $this->_file_upload( $key, $local_path);
311 }
312
313 return $upload;
314 }
315
316 public function save_image_editor_file($override){
317 add_filter( 'wp_update_attachment_metadata', array($this,'image_editor_file_save' ));
318 return $override;
319 }
320
321 public function image_editor_file_save( $metadata ){
322 $metadata = $this->upload_and_thumbs($metadata);
323 remove_filter( 'wp_update_attachment_metadata', array($this, 'image_editor_file_save') );
324 return $metadata;
325 }
326
327 /**
328 * Filters the result when generating a unique file name.
329 *
330 * @since 4.5.0
331 *
332 * @param string $filename Unique file name.
333
334 * @return string New filename, if given wasn't unique
335 *
336 * 参数 $ext 在官方钩子文档中可以使用,部分 WP 版本因为多了这个参数就会报错。 返回“HTTP错误”
337 */
338 public function unique_filename( $filename ) {
339 $ext = '.' . pathinfo( $filename, PATHINFO_EXTENSION );
340 $number = '';
341
342 while ( $this->remote_key_exist( $filename ) ) {
343 $new_number = (int) $number + 1;
344 if ( '' == "$number$ext" ) {
345 $filename = "$filename-" . $new_number;
346 } else {
347 $filename = str_replace( array( "-$number$ext", "$number$ext" ), '-' . $new_number . $ext, $filename );
348 }
349 $number = $new_number;
350 }
351 return $filename;
352 }
353
354 public function sanitize_file_name_handler( $filename ){
355 if ($this->options['opt']['auto_rename']) {
356 return date("YmdHis") . "" . mt_rand(100, 999) . "." . pathinfo($filename, PATHINFO_EXTENSION);
357 } else {
358 return $filename;
359 }
360 }
361
362 /** 根据提交数据进行缩略图设置修改与备份。 (暂时取消在这一步对插件参数更新的步骤,留到后面一起进行更新)
363 * @param $options
364 * @param $set_thumb
365 * @return mixed
366 */
367 private function set_thumbsize_handler($options, $set_thumb){
368 if($set_thumb) {
369 $options['opt']['thumbsize'] = array(
370 'thumbnail_size_w' => get_option('thumbnail_size_w'),
371 'thumbnail_size_h' => get_option('thumbnail_size_h'),
372 'medium_size_w' => get_option('medium_size_w'),
373 'medium_size_h' => get_option('medium_size_h'),
374 'large_size_w' => get_option('large_size_w'),
375 'large_size_h' => get_option('large_size_h'),
376 'medium_large_size_w' => get_option('medium_large_size_w'),
377 'medium_large_size_h' => get_option('medium_large_size_h'),
378 );
379 update_option('thumbnail_size_w', 0);
380 update_option('thumbnail_size_h', 0);
381 update_option('medium_size_w', 0);
382 update_option('medium_size_h', 0);
383 update_option('large_size_w', 0);
384 update_option('large_size_h', 0);
385 update_option('medium_large_size_w', 0);
386 update_option('medium_large_size_h', 0);
387 } else {
388 if(isset($options['opt']['thumbsize'])) {
389 update_option('thumbnail_size_w', $options['opt']['thumbsize']['thumbnail_size_w']);
390 update_option('thumbnail_size_h', $options['opt']['thumbsize']['thumbnail_size_h']);
391 update_option('medium_size_w', $options['opt']['thumbsize']['medium_size_w']);
392 update_option('medium_size_h', $options['opt']['thumbsize']['medium_size_h']);
393 update_option('large_size_w', $options['opt']['thumbsize']['large_size_w']);
394 update_option('large_size_h', $options['opt']['thumbsize']['large_size_h']);
395 update_option('medium_large_size_w', $options['opt']['thumbsize']['medium_large_size_w']);
396 update_option('medium_large_size_h', $options['opt']['thumbsize']['medium_large_size_h']);
397 unset($options['opt']['thumbsize']);
398 }
399 }
400 return $options;
401 }
402
403 private function legacy_data_replace() {
404 if(in_array(get_option('upload_path'), ["", "wp-content/uploads"])){
405 global $wpdb;
406 $originalContent = home_url('/wp-content/uploads');
407 $newContent = get_option('upload_url_path');
408
409 # 文章�
410 容文字/字符替换
411 $result = $wpdb->query(
412 "UPDATE {$wpdb->prefix}posts SET `post_content` = REPLACE( `post_content`, '{$originalContent}', '{$newContent}');"
413 );
414
415 $this->options['opt']['legacy_data_replace'] = 1; # 值为1 表示已完成替换
416 } else {
417 $this->options['opt']['legacy_data_replace'] = 2; # 值为2 表示upload_path非初始默认值,无法替换,建议使用wpreplace插件替换
418 }
419 update_option($this->option_name, $this->options); // 文字替换,参数变动不影响Api实例
420 return $this->options;
421 }
422
423 public function image_display_processing($content){
424 if ( isset($this->options['opt']['img_process'])
425 && $this->options['opt']['img_process']['switch'] ) {
426 $media_url = get_option('upload_url_path');
427 $pattern = '#<img[\s\S]*?src\s*=\s*[\"|\'](.*?)[\"|\'][\s\S]*?>#ims'; // img匹�
428 �正则
429 $content = preg_replace_callback(
430 $pattern,
431 function($matches) use ($media_url) {
432 if (strpos($matches[1], $media_url) === false) {
433 return $matches[0];
434 } else {
435 return str_replace(
436 $matches[1],
437 $matches[1] . $this->image_display_default_tab . $this->options['opt']['img_process']['style_value'],
438 $matches[0]);
439 }
440 },
441 $content);
442 }
443 return $content;
444 }
445
446 private function set_img_process_handle($options, $img_process){
447 if( isset($img_process['img_process_switch']) ){
448 $options['opt']['img_process']['switch'] = True;
449 switch( sanitize_text_field(trim(stripslashes($img_process['img_process_style_choice']))) ){
450 case "0":
451 $options['opt']['img_process']['style_value'] = $this->image_display_default_value;
452 break;
453 case "1":
454 $options['opt']['img_process']['style_value'] = sanitize_text_field(trim(stripslashes($img_process['img_process_style_customize'])));
455 break;
456 }
457 } else {
458 $options['opt']['img_process']['switch'] = False;
459 }
460 return $options;
461 }
462
463 // 在插件列表页添加设置按钮
464 public function setting_plugin_action_links($links, $file) {
465 if ($file == plugin_basename(dirname(__FILE__) . '/index.php')) {
466 $links[] = '<a href="admin.php?page=' . $this->base_folder . '/index.php">设置</a>';
467 }
468 return $links;
469 }
470
471 // 在导航栏“设置”中添加条目
472 public function admin_menu_setting() {
473 add_options_page($this->page_title, $this->menu_title, $this->capability, __FILE__, array($this, 'setting_page'));
474 }
475
476 /**
477 * 插件设置页面
478 */
479 public function setting_page() {
480 // 如果当前用户权限不足
481 if (!current_user_can( $this->capability )) wp_die('Insufficient privileges!');
482
483 $this->options = get_option($this->option_name);
484 if ($this->options && isset($_GET['_wpnonce']) && wp_verify_nonce($_GET['_wpnonce']) && !empty($_POST)) {
485 if($_POST['type'] == 'info_set') {
486 $this->options['bucket'] = isset($_POST['bucket']) ? sanitize_text_field(trim(stripslashes($_POST['bucket']))) : '';
487 $this->options['endpoint'] = isset($_POST['endpoint']) ? sanitize_text_field(trim(stripslashes($_POST['endpoint']))) : '';
488 $this->options['accessKeyId'] = isset($_POST['accessKeyId']) ? sanitize_text_field(trim(stripslashes($_POST['accessKeyId']))) : '';
489 $this->options['accessKeySecret'] = isset($_POST['accessKeySecret']) ? sanitize_text_field(trim(stripslashes($_POST['accessKeySecret']))) : '';
490 $this->options['opt']['auto_rename'] = isset($_POST['auto_rename']);
491 $this->options['no_local_file'] = isset($_POST['no_local_file']);
492
493 $this->options = $this->set_img_process_handle($this->options, $_POST); // 更新数据万象设置,返回options,但未调用update_option
494 $this->options = $this->set_thumbsize_handler($this->options, isset($_POST['disable_thumb']) );
495
496 update_option('upload_url_path', esc_url_raw(trim(stripslashes($_POST['upload_url_path']))));
497 update_option($this->option_name, $this->options);
498 $this->object_storage = new Api($this->options);
499 # 原本想做update_option判断,但�
500 容不改变时返回值为0,会当作失败处理,从业务逻辑上不合理。
501 ?>
502 <div class="notice notice-success settings-error is-dismissible"><p><?php echo($this->setting_notices['update_success']); ?></p></div>
503 <?php
504
505 } else if ($_POST['type'] == 'info_replace') {
506 $this->options = $this->legacy_data_replace();
507 }
508 }
509 require_once('setting.php');
510 }
511 }
512
513 global $WPOSS;
514 $WPOSS = new WPOSS();
515 }
516