类库参考
最后更新于:2022-04-01 05:42:54
Home函数库
最后更新于:2022-04-01 05:42:52
admin函数库
最后更新于:2022-04-01 05:42:49
Common函数库
最后更新于:2022-04-01 05:42:47
### **文件**
因函数比较多在这不全部列举只列举比较常用的函数 Common函数位于/Application/Common/Common/function.php中不懂的可以进去参考
**is_login()**
说明: 检测用户是否已登陆
返回值: integer 0-未登录,大于0-当前登录用户ID
**is_administrator($uid = null)**
说明:检测用户是否是超级管理员
返回值 boolean true-管理员,false-非管理员
**str2arr($str, $glue = ',')**
说明:字符串转换为数组,与explode功能相同,只是参数顺序不同
参数 string $str 要分割的字符串
参数 string $glue 分割符
返回值 array
**arr2str($arr, $glue = ',')**
说明:数组转换为字符串,与implode功能相同,只是参数顺序不同
参数 array $arr 要连接的数组
参数 string $glue 分割符
返回值 strink
**think_encrypt($data, $key = '', $expire = 0)**
说明:数据加密方法,将需要加密的数据串加密为字符串。
参数 string $data 要加密的字符串
参数 string $key 加密密钥
参数 int $expire 过期时间 单位 秒
返回值 string
**think_decrypt($data, $key = '')**
说明:数据解密方法,将think_encrypt加密过的数据还原。
**list_sort_by($list,$field, $sortby='asc')**
说明: 对查询结果集进行排序
参数 array $list 查询结果
参数 string $field 排序的字段名(单个)
参数 array $sortby 排序类型 asc正向排序 desc逆向排序 nat自然排序
返回值 array
**data_auth_sign($data)**
说明:数据签名认证,用于验证提供的数据与存储的数据是否一致。
参数 array $data 被认证的数据
返回值 string 签名
**list_to_tree($list, $pk='id', $pid = 'pid', $child = '_child', $root = 0)**
说明:把返回的查询数据集转换成多维数组的树状结构数据
参数 array $list 要转换的数据集
参数 string $pk 数据集的主键
参数 string $pid 父id数据字段
参数 string $child 用来保存子数据的键名
参数 int $root 根元素的父id
返回值 array
**tree_to_list($tree, $child = '_child', $order='id', &$list = array())**
说明: 将list_to_tree生成的多维数组树状结构数据还原成普通的查询列表
参数 array $tree list_to_tree生成的多维数组树状结构数据
参数 string $child 保存子数据的键名
参数 string $order 排序显示的键,一般是主键 升序排列
参数 array $list 过渡用的中间数组
返回值 array 返回排过序的列表数组
**format_bytes($size, $delimiter = '')**
说明:字节格式化 参数 number $size 字节数 参数 string $delimiter 数字和单位分隔符 返回值 string 格式化后的带单位的大小,例如:2KB
**get_addon_class($name)**
说明:根据插件简单类名返回插件类的命名空间类名
参数 strng $name 插件名
返回值 string
**get_addon_config($name)**
说明:获取插件类的配置数组
参数 string $name 插件名
**addons_url($url, $param = array())**
说明:生成用于访问插件控制器的url
参数 string $url url 例如:'Attachment://Attachment/download'
参数 array $param 参数,例如'id=2'
返回值 string
**time_format($time = NULL,$format='Y-m-d H:i')**
说明:时间戳格式化,与date函数功能相同,只是参数顺序不同
参数 int $time
**get_username($uid = 0)**
说明:根据用户ID获取用户名。首先会尝试从session获取,其次尝试从缓存获取,如果没有获取到,从数据库获取并缓存。
参数 integer $uid 用户ID 返回值 string 用户名
**get_nickname($uid = 0)**
说明:根据用户ID获取用户昵称。首先会尝试从session获取,其次尝试从缓存获取,如果没有获取到,从数据库获取并缓存。
根据用户ID获取用户昵称
参数 integer $uid 用户ID
返回值 string 用户昵称
**get_category($id, $field = null)**
说明:获取某个分类的某个字段的值并缓存
参数 integer $id 分类ID
参数 string $field 要获取的字段名
返回值 string 字段的值
**get_category_name($id)**
说明:根据ID获取分类标识
**get_category_title($id)**
说明:根据ID获取分类名称
**get_document_model($model_id = null, $field = null)**
说明:根据模型id获取文档模型的某个字段的值并缓存
参数 integer $model_id 模型ID
参数 string $field 模型字段
返回值 array
**action_log($action = null, $model = null, $record_id = null, $user_id = null)**
说明:执行该行为的规则,并记录行为日志
参数 string $action 行为标识
参数 string $model 触发行为的模型名,例如:'document'
参数 int $record_id 触发行为的记录id
参数 int $user_id 执行行为的用户id
返回值 boolean
**parse_action($action = null, $self)**
说明: 将一条行为定义文本解析为行为规则数组。
规则定义 table:$table|field:$field|condition:$condition|rule:$rule[|cycle:$cycle|max:$max][;......] 规则字段解释:table->要操作的数据表,不需要加表前缀; field->要操作的字段; condition->操作的条件,目前支持字符串,默认变量{$self}为执行行为的用户 rule->对字段进行的具体操作,目前支持四则混合运算,如:1+score*2/2-3 cycle->执行周期,单位(小时),表示$cycle小时内最多执行$max次 max->单个周期内的最大执行次数($cycle和$max必须同时定义,否则无效) 单个行为后可加 ; 连接其他规则
参数 string $action 行为id或者name
参数 int $self 替换规则里的变量为执行用户的id
返回值 boolean|array: false解析出错 , 成功返回规则数组
**execute_action($rules = false, $action_id = null, $user_id = null)**
说明:执行行为
参数 array $rules parse_action解析后的行为规则数组
参数 int $action_id 行为id
参数 array $user_id 执行的用户id
返回值 boolean false 失败 , true 成功
**create_dir_or_files($files)**
说明:基于数组创建目录和文件,每个目录或文件作为数组的一个元素
**array_column(array $input, $columnKey, $indexKey = null)**
说明:PHP5.5的自带函数兼容实现,来自PHP手册
**get_table_name($model_id = null)**
说明:获取表名(不含表前缀)
参数 string $model_id
返回值 string 表名
**get_model_attribute($model_id, $group = true)**
说明:获取模型属性信息并缓存
参数 integer $id 属性ID
参数 string $field 要获取的字段名
返回值 string 属性信息
**api($name,$vars=array())**
说明:调用系统的API接口的静态方法,例如:api('User/getName','id=5');// 调用公共模块的User接口的getName方法
参数 string $name 格式 [模块名]/接口名/静态方法名
参数 array|string $vars 参数
**get_link($link_id = null, $field = 'url')**
说明: 根据外链id(onethink_url表),返回外链的其他字段值,默认返回url字段值
参数 int $link_id 主键值 参数 string $field 字段值
**get_cover($cover_id, $field = null)**
说明:根据图片id(onethink_picture)返回图片的其他字段值,默认返回所有整行记录
参数 int $cover_id 主键值 参数 string $field 字段名 返回值: 完整的数据 或者 指定的$field字段值
**check_document_position($pos = 0, $contain = 0)**
说明: 文档推荐位检测.检测文档推荐位是否含有含有某个推荐位设置.例如,文档推荐位的值为5,则 check_document_position(5, 1)和`check_document_position(5, 4)`都返回true,而check_document_position(5, 8)返回false.实际上,该函数做的是位检测,所以也可以用于检测其通过位相加保存数据的情况.
参数 number $pos 推荐位的值(数据库保存的字段值) 参数 number $contain 推荐位值(推荐位定义) 返回 true 包含 , false 不包含
函数库参考
最后更新于:2022-04-01 05:42:45
配置参考
最后更新于:2022-04-01 05:42:43
### **分类**
配置 分为针对**模块的配置**和针对**项目的配置**,针对模块的配置定义在模块目录下的config.php文件。而针对项目的配置在后台通过配置管理进行配置。
配置生效的优先级:如果不同类型的配置有重复,项目配置优先级最高,其次是普通模块配置,最后是Common模块的配置。
实际上,在二次开发过程中,需要新增的配置都可以在后台的”配置管理“进行配置。尽量不修改文件配置。除非配置复杂不得不写到配置文件中(例如多维数组配置)
### **Common模块配置**
位于/Application/Common/Conf/config.php里。用来设置全局生效的配置。
| 配置 | 说明 | 当前值 |
| -- | -- | -- |
| AUTOLOAD_NAMESPACE|##########插件安装目录|array('Addons' => ONETHINK_ADDON_PATH)|
| MODULE_DENY_LIST | 禁止通过路径访问 | array('Common','Admin','Install') |
| DATA_AUTH_KEY | 默认的加密key | &up;v8"1x0]!q#OG/(hjD@N{LY+-n}fTUkm*tsJ_ |
| DEFAULT_THEME | 默认主题名 | Default |
| USER_MAX_CACHE | 最大缓存用户数 | 1000|
| USER_ADMINISTRATOR | 管理员用户ID | 1 |
| URL_CASE_INSENSITIVE| 默认false 表示URL区分大小写 true则表示不区分大小写 | false |
| URL_MODEL| URL模式 | 3 |
| VAR_URL_PARAMS| PATHINFO URL参数变量 | '' |
| URL_PATHINFO_DEPR | PATHINFO URL分割符 | '/' |
| DEFAULT_FILTER| 全局过滤函数 | ''|
| 'DOCUMENT_MODEL_TYPE' | 文档模型配置 (文档模型核心配置,请勿更改) | array(2 => '主题', 1 => '目录', 3 => '段落') |
### **Home模板配置**
位于/Application/Home/Conf/config.php里。用来设置只针对Home模块的配置
![2015-08-05/55c1aa7bcb0d9](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-05_55c1aa7bcb0d9.png)
### **Admin模块配置**
位于/Application/Admin/Conf/config.php里。用来设置只针对Admin模块的配置
| 配置名 | ##########说明 | 当前值 |
| -- | -- | -- |
| DEFAULT_THEME | 默认模板主题名称 | '' |
| DATA_CACHE_PREFIX| 缓存前缀 | sent_ |
| DATA_CACHE_TYPE | 数据缓存类型 | File |
| URL_MODEL | URL模式 |3 |
| DOWNLOAD_UPLOAD | 文件上传相关配置 | 数组,配置了后台文件上传的类型、大小、路径、命名方式等。 |
| PICTURE_UPLOAD | 图片上传相关配置 | 数组,配置了后台图片上传的类型、大小、路径、命名方式等。|
| PICTURE_UPLOAD_DRIVER | 图片上传启动器 |local |
| EDITOR_UPLOAD | 编辑器图片上传配置 | 数组 |
| UPLOAD_LOCAL_CONFIG | 本地上传驱动配置 | 见配置文件 |
| TMPL_PARSE_STRING | 模板相关配置 | 见配置文件 |
| SESSION_PREFIX | session前缀 | sent_admin |
| COOKIE_PREFIX | Cookie前缀 避免冲突 | sent_admin_ |
| VAR_SESSION_ID | 修复uploadify插件无法传递session_id的bug | session_id|
|TMPL_ACTION_ERROR| 默认错误跳转对应的模板文件 | MODULE_PATH.'View/Public/error.html |
| TMPL_ACTION_SUCCESS | 默认成功跳转对应的模板文件 | MODULE_PATH.'View/Public/success.html |
附录
最后更新于:2022-04-01 05:42:40
文档模型扩展
最后更新于:2022-04-01 05:42:38
### **如何增加新的文档模型**
在**模型管理** 里新增一个模型,类型选择 **文档模型**。假设标识为“topic”,名称为“话题”。
![**文档模型**](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0857225c46.png)
编辑上述模型,新增业务所需的属性。
![2015-08-04/55c087241c31f](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c087241c31f.png)
1.在Application/Admin/logic文件夹下新建 TopicLogic.class.php 类,该类需继承 BaseLogic。然后在该类中编写所需的业务逻辑(可参考已有的ArticleLogic.class.php)。
>注意BaseLogic里的两个抽象方法需要手动实现。
在分类管理里,新增和编辑分类时就可以选择上面新建的话题模型了
![2015-08-04/55c087c3ba17f](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c087c3ba17f.png)
最后,我们就可以在内容里新增刚才建立的 话题。
独立模型扩展
最后更新于:2022-04-01 05:42:36
### **通过模型管理新增模型**
### **自己建表,然后生成模型**
模型扩展开发指南
最后更新于:2022-04-01 05:42:33
### **建立模型**
在模型管理里可新建一个自定义模型,也可以新建文档模型。
**文档模型的本质是继承自基础文档模型。**
插件开发注意事项
最后更新于:2022-04-01 05:42:31
1.插件的基础是物理文件,所以插件类的结构一定要和官方一致。开发阶段不要随便删除文件,导致系统运行不正常。要删除插件请先后台卸载插件后再删除物理文件,否则钩子表有脏数据或者叫未挂载的插件,进钩子列表删除。
2.插件的资源文件的存放,首先插件本身应当保存原始静态资源,然后 安装的时候,通过install 方法 移动到wwwroot/Public/Home/Addons/下,然后用__PUBLIC__常量拼好资源路径去获取。如果不想移动,模板中可以用__ADDONROOT__ 表示插件根目录。
3.整个产品的上传目录在/Uploads下,编辑器统一上传到下面的Editor目录
里。并且配置好上传后缀,比如只允许图片的后缀,发布时没定义后缀,被人上传了php文件篡改了系统文件,是个严重的漏洞。安全由二次开发者自己把握。
钩子里挂载的插件是可以排序和删除的。如钩子的编辑页,见下图:
![2015-08-04/55c0822b20ae8](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0822b20ae8.png)
所以当一个插件安装失败了,钩子上写入了该插件,没事直接去钩子配置里删除整个插件挂载就行了。 而且同一个展示的钩子上挂载了多个插件,直接后台排个序更新下就生效执行的顺序了。不用去数据库改了,方便吧。
1.后台首页的钩子,上样式是公用的,如果想扩展这样的插件,直接复制SystemInfo插件改改类名和显示内容就可以了,改好安装就行了。显示宽度配置里可以设置1、2、4格子。以后会提供刷新接口和最小化js函数。等正式版发布了,同步对比下那个插件就好了。
2.扩展这块属于开发人员负责,所以钩子的编辑更新,只有超级管理员可以管理,其他人没有权限编辑的。乱加、乱删都会影响系统稳定性和效率。
3.注意插件类里assign复制的变量在真个程序模板中有和相应页面控制器的模板的一样的作用域,因此会存在变量被污染的情况,就是我在插件里赋值一个data,在插件前的控制器里赋值data变量就被插件污染了,会造成开发上很大的困惑,因此我们强烈建议大家给插件向模板赋值的时候加上addons_前缀,这样被污染的机率小很多了。
插件后台的开发
最后更新于:2022-04-01 05:42:29
如果插件需要后台显示,可以在快速创建插件步骤里,勾上‘是否需要后台列表’,然后配置下列表显示需要的数组参数,如果需要制定模板,可以写下custom_adminlist文本域,指定列表页的模板。然后可以在那里二次开发,比如弄个弹窗更新部分数据什么的。
这个你们可以将Attachement 附件 插件的那个custom_adminlist属性去了看有显示,加上也有显示。 具体实现可以看AddonsController里的adminlist方法和对应模板。
插件后台里的新增、更新页面后的跳转地址可以通过cookie('__forward__')去获取后台列表的url。
插件不一定有设置页面。必须插件目录里有一个config.php并且返回一个非空的数组才会显示。设置页里有显示。
拿Editor插件的配置文件举列子:
~~~
<?php
// +----------------------------------------------------------------------
// | OneThink [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2013 http://www.onethink.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: yangweijie <yangweijiester@gmail.com> <code-tech.diandian.com>
// +----------------------------------------------------------------------
return array(
'editor_type'=>array(
'title'=>'编辑器类型:',
'type'=>'select',
'options'=>array(
'1'=>'普通文本',
'2'=>'富文本',
'3'=>'UBB解析',
'4'=>'Markdown编辑器'
),
'value'=>'1',
),
'editor_wysiwyg'=>array(
'title'=>'富文本编辑器:',
'type'=>'select',
'options'=>array(
'1'=>'Kindeditor',
'2'=>'Ueditor(百度编辑器)',
),
'value'=>1
),
'editor_height'=>array(
'title'=>'编辑器高度:',
'type'=>'text',
'value'=>'300px'
),
'editor_resize_type'=>array(
'title'=>'是否允许拖拉编辑器',
'type'=>'radio',
'options'=>array(
'0'=>'不允许',
'1'=>'允许'
),
'value'=>'1',
'tip'=>'ubb和markdown编辑器不支持此功能'
),
);
~~~
数组的每个键都对应一个form表单。键名就是配置里会显示的表单名 并且为了防止冲突加了前缀 ,比如上面的editor_type 会显示为
config[editor_type]。title是字段前面的标识字。type是form标准的type,出了一个扩展的group。待会单独讲group。然后有多个选项的会有options键和值是相应选项的数组。值里每个键是选项的value后面的值是显示的label文字。value字段是该表单项的默认值。 tip是表单项里面的灰色提示文字。
效果图如下:
![2015-08-04/55c08011dbf41](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c08011dbf41.png)
实际解析的方法在Admin/View/Addons/config.html里。
Group类型是我为了插件中会去配置多个同类型事物的配置,每次要加前缀导致的混乱。正好用到后台的tab。 每个group里值options就是多个配置分组tab。然后分组显示名是其title。然后options里是每个配置的复合数组。那个写法和之前单独的配置一样。 代码如下:
~~~
return array(
'comment_type'=>array(//配置在表单中的键名 ,这个会是config[random]
'title'=>'使用类型:', //表单的文字
'type'=>'select', //表单的类型:text、textarea、checkbox、radio、select等
'options'=>array( //select 和radion、checkbox的子选项
'1'=>'有言', //值=>文字
'2'=>'多说',
),
'value'=>'1', //表单的默认值
),
'group'=>array(
'type'=>'group',
'options'=>array(
'youyan'=>array(
'title'=>'友言配置',
'options'=>array(
'comment_uid_youyan'=>array(
'title'=>'账号id:',
'type'=>'text',
'value'=>'90040',
'tip'=>'填写自己登录友言后的uid,填写后可进相应官方后台'
),
)
),
'duoshuo'=>array(
'title'=>'多说配置',
'options'=>array(
'comment_short_name_duoshuo'=>array(
'title'=>'短域名',
'type'=>'text',
'value'=>'',
'tip'=>'每个站点一个域名'
),
'comment_form_pos_duoshuo'=>array(
'title'=>'表单位置:',
'type'=>'radio',
'options'=>array(
'top'=>'顶部',
'buttom'=>'底部'
),
'value'=>'buttom'
),
'comment_data_list_duoshuo'=>array(
'title'=>'单页显示评论数',
'type'=>'text',
'value'=>'10'
),
'comment_data_order_duoshuo'=>array(
'title'=>'评论显示顺序',
'type'=>'radio',
'options'=>array(
'asc'=>'从旧到新',
'desc'=>'从新到旧'
),
'value'=>'asc'
)
)
)
)
)
);
~~~
当然如果你觉的这样解析不够你的效果。你可以在Addons定义类里加个custom_config属性。其值是配置页会包含的模板。然后你可以使用各种js特效样式了。这个可以看返回顶部的配置页(wwwroot/Addons/ReturnTop/config.html)。
插件配置自定义,只需要输出中间的表单即可。当然可以带上js。在次自定义模板中可以输出的是$data。
$data[config]是所有的从配置数组和数据库合并过的数组。模板里{$data.config.name.value}是输出配置项里name的那个表单项的值。
插件的开发流程
最后更新于:2022-04-01 05:42:27
### **理解插件的含义**
先通读《插件开发指南》,理解插件和钩子的含义,以及插件的安装、执行、配置、卸载流程。
### **弄清自己待开发的插件需求-要完成的逻辑业务**
问自己几个问题:
1.插件的基本信息(名称、标识、描述、状态、作者、版本)是哪些
2.插件的用途是扩展显示还是控制数据
3.插件对使用控制器是否有要求,要不要单独的数据表(要调用插件的模型)
4.插件要用哪些钩子、一个还是多个
5.插件需要配置不,要的话,有哪些选项,要不要用分组,要不要自定义配置显示模板
6.插件需要后台不,默认显示数据,还是高级后台(有自己的管理界面和详情等)
### **快速创建插件,进行插件的完善**
进入“扩展”-》“插件管理”点“快速创建”进入创建阶段,填写基本信息,按照自身的需求、想好的问题勾选适当的选项。 配置目前还没做到可视化,大家先默认创建好文件,然后去文件里修改,都是数组,参照“插件后台开发”里的配置说明。
### **例子**
首先清楚插件就是符合一定规范拥有一定目录结构的类文件。
然后清楚自己要开发的插件的要求,在什么位置显示、是否有配置文件、是否需要外部url访问、是否需要后台显示。
接下来我们就用后台快速开发一个Example插件示范。
首先,进入后台->扩展->插件列表->快速创建。会出现一个如下的页面。
![2015-08-04/55c07e26addd0](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c07e26addd0.png)
由于仅作简单的示范,我们不需要配置文件和外部控制器,都用默认的。钩子我们选‘documentDetailAfter’;最新版已经支持多选了。
可以点“预览”按钮看下将要生成的类文件,如下图:
![2015-08-04/55c07e6d9b09a](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c07e6d9b09a.png)
点确定后,出现“创建成功”,并且列表里出现“示列”这个插件表示插件已经创建成功了。
然后我们可以看见生成的类文件 documentDetailAfter方法里什么都没有。我们在里面输出点文字,见下图:
![2015-08-04/55c07ec426f56](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c07ec426f56.png)
其实和我们开发widget方法没啥太大区别。只不过这里前台用hook函数调用类里的钩子方法。
因为本产品使用了命名空间,所以插件类的引入需要通过命名空间的写法,才能正确自动加载类。而我们的快速创建就是为你们指定好插件定义信息后,自动创建一些规范的目录文件。所以你们只需要考虑插件的一些信息即可。
如果需要外部url 访问插件里的控制器。需要用addons_url(‘插件名://控制器名/方法’) 这样访问控制器。控制器里必须继承Home/AddonsController类。不能继承后台的。那个里面很多方法插件用不到,是为后台所定制的。要使用,只需在创建时候勾选‘是否需要外部访问’,就会自动创建控制器目录和文件,还有Model目录。然后自己按需修改吧。
什么是钩子?
最后更新于:2022-04-01 05:42:24
讲到插件,不得不讲钩子。首先,我们之前说明了插件是一个扩展的功能实现。
既然是扩展的,那么就要很灵活、可复用,并不是像我们之前开发项目,一个功能实现了,就写死在代码里了。
项目其他地方要用了,怎么办,复制一份改个名,改的那个地方能调用实现。这样一次两次可以,次数多了就不行了。
因为后面每次开发的底层架构在不断变化。不断重复的功能版本造成人力的浪费。我们做成插件的目的就是为了方便大家扩展我们这个产品的功能。到时候形成规模,大家自由的搭建自己的站点就方便了。
那么如何让一个扩展的功能在多个地方可随意的使用呢。那就用到了我们的钩子。
为什么叫它钩子呢?因为它的作用就是如此和生活中的钩子类似。
打个比方,我们做的网站比作一个有多个功能的立式衣架。
这个衣架给什么人用就有不同的用途。
假如你专门用来挂大衣的,那就是大衣衣架。如果你专门挂袋子,那就是一个储物衣架。
当你不想要某个挂件、衣服时,取下来即可。并不会破坏原有的袋子或者衣服的功能。
你挂与不挂,钩子就在那里。
为什么能挂那么多东西呢?说明被挂的东西都符合一个标准:能挂的住。
换作你挂一个橡皮泥、或者棉花之类的。挂不了多久就会掉了。因为他们不符合要有部分封闭的可固定的这一个部分的标准。
还有挂一个太重的比如10个背包挂一个钩子上。要么架子毁了,要么钩子断了。总之就是挂不住。
因为任何一个钩子都有其承重上限。你加起来的超过了,肯定不行。
所以我们不能把插件当成万能的使,什么东西都整成插件,不管功能的大小。
任何系统都有瓶颈,你不能把个重量级的东西做成插件后挂上,说不定以后就会影响整个站点。就违背了插件的独立性原则。那些就不应该做成插件而是做成模型扩展或者应用扩展。
什么是插件?
最后更新于:2022-04-01 05:42:22
插件是用于扩展系统的功能的一些独立“组件”。
### **功能定义**
插件的定位是用于实现某些简单的显示及数据处理的功能扩展。所以我们的初衷是插件的开启关闭,不会影响原有数据。
为了管理的方便,我们在后台给插件默认提供了插件列表页,钩子列表,以及配置插件、快速创建插件结构、安装、卸载、启用停用、插件后台页面,方便大家定义管理插件相关的数据。
### **物理定义**
位于站点根目录 wwwroot/Addons 下的一个类库,可以被系统的hooks函数访问到。
目录结构如以下(以自带的Editor插件为示范):
~~~
|-wwwroot
| |-Addons [所有插件目录]
| | |-Editor [插件目录]
| | | |-Controller [控制器目录,有URL访问的时候才需要,可选]
| | | | |-UploadController.class.php [插件控制器,名字可以不和插件名一样]
| | | |-config.php [配置文件, 有配置项的话可选]
| | | |-content.html [插件模板页面,可选]
| | | |-EditorAddons.class.php [插件定义和实现的文件,必须有!!]
| | | |-[adminlist.html、config.html] [这些模板可选,用于自定义插件的配置页和列表页]
~~~
目前仅介绍一下目录结构,在插件开发中将讲解每个文件如何定义。
插件开发指南
最后更新于:2022-04-01 05:42:20
### **完整的插件运行流程**
**插件安装流程**
首先 ,我们打开Editor插件的定义类
~~~
<?php
// +----------------------------------------------------------------------
// | OneThink [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2013 http://www.onethink.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: yangweijie <yangweijiester@gmail.com> <code-tech.diandian.com>
// +----------------------------------------------------------------------
namespace Addons\Editor;
use Common\Controller\Addon;
/**
* 编辑器插件
* @author yangweijie <yangweijiester@gmail.com>
*/
class EditorAddon extends Addon{
public $info = array(
'name'=>'Editor',
'title'=>'前台编辑器',
'description'=>'用于增强整站长文本的输入和显示',
'status'=>1,
'author'=>'thinkphp',
'version'=>'0.1'
);
public function install(){
return true;
}
public function uninstall(){
return true;
}
/**
* 编辑器挂载的文章内容钩子
* @param array('name'=>'表单name','value'=>'表单对应的值')
*/
public function documentEditFormContent($data){
$this->assign('addons_data', $data);
$this->assign('addons_config', $this->getConfig());
$this->display('content');
}
/**
* 讨论提交的钩子使用编辑器插件扩展
* @param array('name'=>'表单name','value'=>'表单对应的值')
*/
public function topicComment ($data){
$this->assign('addons_data', $data);
$this->assign('addons_config', $this->getConfig());
$this->display('content');
}
}
~~~
整个插件就是一个特殊的继承了 Addon抽象类的子类。必须实现 install和uninstall方法。 然后必须有一个自己的info属性,作为插件自己的信息。name、titile、description、status、author、version这6个是必须的。到时候后台列表里在未安装时会读取插件信息,显示出来。status为1或者0,表示安装插件后是否立即启用。
install和uninstall方法用于后台插件安装和卸载时候调用。返回true或者false用于告诉后台我安装卸载的准备工作是否做好了。比如我安装时候创建了某些表,创建成功可以安装,不成功提示错误。卸载前应该将安装时做的操作恢复到安装前状态。 其次,插件被安装后才能配置插件,卸载后会同时去除钩子处挂载的插件名,安装会添加钩子对应的插件名。
后台AddonsController.class.php
~~~
/**
* 安装插件
*/
public function install(){
$addon_name = trim(I('addon_name'));
$class = get_addon_class($addon_name);
if(!class_exists($class))
$this->error('插件不存在');
$addons = new $class;
$info = $addons->info;
if(!$info || !$addons->checkInfo())//检测信息的正确性
$this->error('插件信息缺失');
session('addons_install_error',null);
$install_flag = $addons->install();
if(!$install_flag){
$this->error('执行插件预安装操作失败'.session('addons_install_error'));
}
$addonsModel = D('Addons');
$data = $addonsModel->create($info);
if(is_array($addons->admin_list) && $addons->admin_list !== array()){
$data['has_adminlist'] = 1;
}else{
$data['has_adminlist'] = 0;
}
if(!$data)
$this->error($addonsModel->getError());
if($addonsModel->add($data)){
$config = array('config'=>json_encode($addons->getConfig()));
$addonsModel->where("name='{$addon_name}'")->save($config);
$hooks_update = D('Hooks')->updateHooks($addon_name);
if($hooks_update){
S('hooks', null);
$this->success('安装成功');
}else{
$addonsModel->where("name='{$addon_name}'")->delete();
$this->error('更新钩子处插件失败,请卸载后尝试重新安装');
}
}else{
$this->error('写入插件数据失败');
}
}
~~~
实例化插件类->插件info是否正确->执行install方法,预安装操作->添加插件数据到数据库Addons表和Hooks表
每个步骤出错都会提示,install 方法中错误用 session('addons_install_error', 'error')传递。
**插件卸载流程**
卸载流程和安装相反
后台AddonsController.class.php
~~~
/**
* 卸载插件
*/
public function uninstall(){
$addonsModel = M('Addons');
$id = trim(I('id'));
$db_addons = $addonsModel->find($id);
$class = get_addon_class($db_addons['name']);
$this->assign('jumpUrl',U('index'));
if(!$db_addons || !class_exists($class))
$this->error('插件不存在');
session('addons_uninstall_error',null);
$addons = new $class;
$uninstall_flag = $addons->uninstall();
if(!$uninstall_flag)
$this->error('执行插件预卸载操作失败'.session('addons_uninstall_error'));
$hooks_update = D('Hooks')->removeHooks($db_addons['name']);
if($hooks_update === false){
$this->error('卸载插件所挂载的钩子数据失败');
}
S('hooks', null);
$delete = $addonsModel->where("name='{$db_addons['name']}'")->delete();
if($delete === false){
$this->error('卸载插件失败');
}else{
$this->success('卸载成功');
}
}
~~~
实例化插件类->执行预卸载->卸载插件所挂载过的钩子处信息->删除插件文件。 每个步骤出错都会提示,install 方法中错误用 session('addons_uninstall_error', 'error')传递。
**插件运行流程**
hook函数调用Hook类的listen静态方法触发钩子->获取钩子挂载的开启的插件->执行对应插件实现的钩子同名方法(这个在init_hooks函数会初始化)
代码上就是如下的过程:
>hook('documentEditFormContent');
然后hook函数遍历 Hook类里的$tag属性,知道有哪些插件可被调用,接下来去读取配置和状态,启用就去执行钩子
~~~
/**
* 处理插件钩子
* @param string $hook 钩子名称
* @param mixed $params 传入参数
* @return void
*/
function hook($hook,$params=array()){
\Think\Hook::listen($hook,$params);
}
而Hook::listen方法里面
/**
* 监听标签的插件
* @param string $tag 标签名称
* @param mixed $params 传入参数
* @return void
*/
static public function listen($tag, &$params=NULL) {
if(isset(self::$tags[$tag])) {
if(APP_DEBUG) {
G($tag.'Start');
trace('[ '.$tag.' ] --START--','','INFO');
}
foreach (self::$tags[$tag] as $name) {
APP_DEBUG && G($name.'_start');
$result = self::exec($name, $tag,$params);
if(APP_DEBUG){
G($name.'_end');
trace('Run '.$name.' [ RunTime:'.G($name.'_start',$name.'_end',6).'s ]','','INFO');
}
if(false === $result) {
// 如果返回false 则中断插件执行
return ;
}
}
if(APP_DEBUG) { // 记录行为的执行日志
trace('[ '.$tag.' ] --END-- [ RunTime:'.G($tag.'Start',$tag.'End',6).'s ]','','INFO');
}
}
return;
}
/**
* 执行某个插件
* @param string $name 插件名称
* @param Mixed $params 传入的参数
* @return void
*/
static public function exec($name, $tag,&$params=NULL) {
if(false === strpos($name,'\\')) {
// 插件(多个入口)
$class = "Addons\\{$name}\\{$name}Addon";
}else{
// 行为扩展(只有一个run入口方法)
$class = $name.'Behavior';
$tag = 'run';
}
$addon = new $class();
return $addon->$tag($params);
}
#实例化一个插件类,我们有get_addon_classs()函数,传入插件名即可或得插件类名,然后直接new 就行了,区分大小写
$addons_class = new get_addon_classs($addon_name);
~~~
$addon->$tag() 这个方法就是去执行钩子处挂载的和钩子同名的插件方法。
这个方法里可以display渲染模板,默认插件的模板就在插件目录下,比如Editor插件类里用的$this->display('content');。
如果你想有目录层可以传入目录/模板这样的参数,这样是不支持主题的,如果要支持主题,也可以传入具体的模板路径,使用T函数如T('Addons://Attachment@Article/edit')这样,到时候想切换主题了,T函数定位模板之前C('DEFAULT_THEME','default')这样就行了。
当然这个方法支持传参,只允许一个,为了能实现引用。所以多个参数,请封装成数组,传入hook函数的第二个参数。
执行完毕,这个钩子的某个插件前台功能方法就运行完了。
插件被禁用,钩子处的插件不会被执行该同名方法,并且插件后台列表里不会出现该插件的列表。
插件不光前台能用 ,后台有钩子,并且插件里实现了该钩子,也可以用。为了前后台编辑器插件可以配置不同的编辑器,我们复制了一份Editor插件,改名为EditroForAdmin了。
还有后台首页,其实是用AdminIndex 钩子挂载了几个插件。
后台中,插件默认会在 插件列表里出现。默认没有安装过的会显示在前面。
基本的数据字段都是读取的插件类里的info属性数组。插件未安装的时候是不可以设置的。
假如插件需要url访问,就必须插件里有Controller目录和tp结构一样。 只不过生成这个方法的用addons_url('插件名://控制器名/操作方法'),生成访问url。并且访问权限由插件去做。具体写法,参照附件Attachment插件 里的控制器写法。和模板里url调用。
权限管理指南
最后更新于:2022-04-01 05:42:18
###**权限扩展**
### **sentCMS权限判断流程按顺序如下:**
**1.IP 权限检测**,如果配置了IP白名单(ADMIN_ALLOW_IP),则仅有白名单的IP可以通过这一步检测。其他IP被直接拒绝。
**2.特殊节点检测,**特殊节点有两种。一种是任何管理员都可以访问的节点(ALLOW_VISIT),例如修改密码; 一种是除超级管理员外,任何管理员都不能访问的节点(DENY_VISIT), 例如仅供超级管理员使用的功能或者某些不提供外部访问但因为编码需要不得不定义为public的方法。
如果ALLOW_VISIT检测通过,直接放行访问。如果DENY_VISIT检测通过,直接拒绝。如果二者都未通过,则会进入下一步权限检测。
PS:在二次开发完成后,应仔细检查每个控制的公共方法,包括继承的公共方法。确保每个公共方法,出现在菜单,ALLOW_VISIT,DENY_VISIT之一。
**3.动态检测**,即有些url是后来动态的,例如分类,不可能我们每创建一个分类就为这个分类添加一个节点,更不可能每添加一篇文档就为这篇文档添加一个节点,这样不仅管理麻烦而且节点数量也会爆炸性增长。因此,需要写一段程序来检测此类url,详细扩展方法见下文。
**4.菜单节点检测**,即通过菜单”管理配置“的节点,在”权限管理->访问授权“进行设置控制。检测通过后放行,否则拒绝。
### **动态权限检测扩展**
**相关数据表**
**auth_extend**
**group_id** 保存用户组id
**type** 权限扩展类型,"分类授权"的type为1,如果要扩展授权,您要为您的扩展权限类型规定一个type值,与其他类型的扩展授权区分。
**extend_id** 保存用户组关联的扩展数据的id,对"分类授权"而言,该字段保存的自然就是分类的id了
**模型代码编写**
您需要在 AuthGroupModel 模型中定义一个静态方法,供控制器查询某个用户对某个类型的扩展权限拥有权限的数据id。
以"分类授权"为例,AuthGroupModel::getAuthCategories(UID) 方法做了如下事情:
1.根据用户id,查出用户所属用户组group_id
2.根据group_id,从auth_extend表以type=1为条件查出了extend_id,即用户拥有权限的分类.
另外,您需要定义一个方法,用来保存权限设定。
以"分类授权"为例,AuthGroupModel::addToCategory($category_id,$uid) 即用来把分类的权限设定写入auth_extend表
**控制器代码编写**
用来执行动态检测的程序代码,定义在相应控制器的 checkDynamic方法,因此,需要扩展动态权限检测时,只需要在你的控制器中创建checkDynamic方法,并在其中实现检测业务逻辑。sentCMS的分类和文档的权限控制即通过在ArticleController中定义checkDynamic方法实现。
与模板有关的控制器代码编写就不再介绍。
以"分类授权"为例,相关方法如下:
1.addToCategory() 为用户授权某个分类
2.category() 输出分类授权设定页面
**模板代码编写**
1.修改View/AuthManager/index.html,增加一个链接,指向具体的权限设定页面
例如:"分类授权"的代码:<a href="{:U('AuthManager/category?group_name='.$vo['title'].'&group_id='.$vo['id'])}" >分类授权</a>
2.创建您的授权设定页模板,分类授权设置页模板位于 View/AuthManager/category.html
模板开发指南
最后更新于:2022-04-01 05:42:15
**基础知识**
见 ThinkPHP3.2手册 视图部分
**sentCMS后台模板**
后台使用了模板继承,基础模板位于 View/Public/base.html。其他模板都位于对应的控制器模板目录。
后台公共js文件位于Public/Admin/Js/common.js,这个js文件是在页面主体内容之后加载。
后台公共css文件位于Public/Admin/Css/base.css
在具体的控制器模板中,你可以通过继承重置<block name="script"></block>来向当前模板加入新的js。这些代码在common.js之后执行。
**sentCMS前台模板**
前台模板比较少,通常二次开发时会删除所有模板,根据设计重新组织和编写前台模板,所以这里对模板文件和目录不做介绍
**Article标签库**
~~~
'partlist' => array('attr' => 'id,field,page,name', 'close' => 1), //段落列表
'partpage' => array('attr' => 'id,listrow', 'close' => 0), //段落分页
'prev' => array('attr' => 'name,info', 'close' => 1), //获取上一篇文章信息
'next' => array('attr' => 'name,info', 'close' => 1), //获取下一篇文章信息
'page' => array('attr' => 'cate,listrow', 'close' => 0), //列表分页
'position' => array('attr' => 'pos,cate,limit,filed,name', 'close' => 1), //获取推荐位列表
'list' => array('attr' => 'name,category,child,page,row,field', 'close' => 1), //获取指定分类列表
~~~
**Article:list 标签补充说明**
category 属性 最好是带单引号, 如 category="'0'" 因为标签库里判断了empty, 可能导致0 相当于没传参数
**Article:position 标签补充说明:**
~~~
pos 是数字,后台有 :
1:列表推荐
2:频道推荐
4:首页推荐
~~~
所以一般前台读取的时候,一般传指定的数字如 pos="1" 这种
而cate 这边比较坑, 默认你传指定的45 单数字没问题, 传多个分类 45,46 这样不行的,必须带单引号 如 cate="'45,46'"
公共函数和库函数使用规范
最后更新于:2022-04-01 05:42:13
## 函数
### 函数名
函数命名,全部使用小写,单词直接使用 _ 连接,函数名的名称应与函数功能相符,且函数名中使用的单词,应该是全称单词.
### 函数文件
二次开发时增加的函数,尽量使用独立的新函数文件保存,通过配置载入,以方便未来的升级.
### 代码质量
由于函数通常会被多次调用,因此应确保函数代码质量,仔细检测测试保证函数在执行过程中不会抛出任何级别的PHP错误,否则部署后可能引起严重的性能问题.
### 其他
函数应该少而精干,数据的增删改查,尽量通过模型方法实现,以利于代码管理维护.通常定义函数是为了在模板中查询和转换数据,或者在不同模块之间实现代码重用.
## 类库
### 模块类库
模块的类库,即Application目录下的控制器,模型等类。文件和类名的命名规则不变,只是控制器命名默认使用Controller代替了Action,如果还想用Action,在Common/Config/config.php 中配置 DEFAULT_C_LAYER=>'Action'
模块类库的命名空间定义见: 控制器定义 和 模型定义
### 插件类库
即Addons目录下插件的类,文件模板如下:
文件名:Addons/插件名/插件名Addon.class.php
~~~
<?php
namespace Addons\插件名;
use Common\Controller\Addon;
class 插件名Addon extends Addon{
}
~~~
文件名:Addons/插件名/Controller/控制器名Controller.class.php
~~~
<?php
namespace Addons\插件名\Model;
use Think\Model;
class 模型名Model extends Model{
}
~~~
## sentCMS框架类库
sentCMS基于ThinkPHP3.2,类库文件位于ThinkPHP/Library目录,框架类库全部使用命名空间载入,在控制器和模型中使用框架类库时,只需代码前面用 use 声明要载入的类库的命名空间即可.
关于命名空间,参考ThinkPHP3.2文档
您可以在 ThinkPHP/Library目录下创建新的目录放置其他类库文件,相应地这些类库文件必须修改类库的命名空间定义和访问.
数据字典
最后更新于:2022-04-01 05:42:11
**1、系统行为表 (sent_action)**
![2015-08-04/55c0700b8b843](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0700b8b843.png)
**2、行为日志表 (sent_action_log)**
![2015-08-04/55c07025e20ac](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c07025e20ac.png)
**3、广告表 (sent_ad)**
![2015-08-04/55c070493c5a6](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c070493c5a6.png)
**4、广告位表 (sent_ad_place)**
![2015-08-04/55c0705caeb04](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0705caeb04.png)
**5、插件表 (sent_addons)**
![2015-08-04/55c0707d077a1](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0707d077a1.png)
**6、附件表 (sent_attachment)**
![2015-08-04/55c070a025bd7](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c070a025bd7.png)
**7、模型属性表 (sent_attribute)**
![2015-08-04/55c070bae2e4e](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c070bae2e4e.png)
![2015-08-04/55c070c5e68ba](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c070c5e68ba.png)
**8、用户组与分类的对应关系表 (sent_auth_extend)**
![2015-08-04/55c070e0f2d18](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c070e0f2d18.png)
**9、 (sent_auth_group)**
![2015-08-04/55c070fa2c8a5](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c070fa2c8a5.png)
**10、 (sent_auth_group_access)**
![2015-08-04/55c0713f7f289](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0713f7f289.png)
**11、 (sent_auth_rule)**
![2015-08-04/55c0715980847](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0715980847.png)
**12、分类表 (sent_category)**
![2015-08-04/55c0718b5e0d2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0718b5e0d2.png)
![2015-08-04/55c07193e5898](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c07193e5898.png)
**13、 (sent_channel)**
![2015-08-04/55c071cb62020](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c071cb62020.png)
**14、 (sent_config)**
![2015-08-04/55c071e468d46](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c071e468d46.png)
**15、文档模型基础表 (sent_document)**
![2015-08-04/55c071fe70bbd](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c071fe70bbd.png)
![2015-08-04/55c07207e7d10](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c07207e7d10.png)
**16、文档模型文章表 (sent_document_article)**
![2015-08-04/55c07237b9b43](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c07237b9b43.png)
**17、文档模型下载表 (sent_document_download)**
![2015-08-04/55c07246c6c99](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c07246c6c99.png)
**18、文件表 (sent_file)**
![2015-08-04/55c07262b7ce2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c07262b7ce2.png)
**19、 (sent_hooks)**
![2015-08-04/55c0727e87063](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0727e87063.png)
**20、会员表 (sent_member)**
![2015-08-04/55c0729aeae83](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0729aeae83.png)
**21、 (sent_member_extend)**
![2015-08-04/55c072b8916f4](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c072b8916f4.png)
**22、 (sent_member_extend_group)**
![2015-08-04/55c072e1091e7](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c072e1091e7.png)
**23、 (sent_member_extend_setting)**![**23、 (sent_member_extend_setting)**](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0733670979.png)
![2015-08-04/55c07344425e2](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c07344425e2.png)
**24、 (sent_menu)**
![2015-08-04/55c0738a63539](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0738a63539.png)
**25、文档模型表 (sent_model)**
![2015-08-04/55c073aa2439e](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c073aa2439e.png)
![2015-08-04/55c073b3c55d1](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c073b3c55d1.png)
**26、模块管理表 (sent_module)**
![2015-08-04/55c073d12d8a0](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c073d12d8a0.png)
**27、 (sent_page)**
![2015-08-04/55c073fcc9a3c](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c073fcc9a3c.png)
**28、 (sent_picture)**
![2015-08-04/55c0740e35c46](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0740e35c46.png)
**29、 (sent_seo_rule)**
![2015-08-04/55c0742ab37c8](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0742ab37c8.png)
![2015-08-04/55c0745207d93](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0745207d93.png)
![2015-08-04/55c07472e502c](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c07472e502c.png)
![2015-08-04/55c0748b49c8a](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-04_55c0748b49c8a.png)