错误提示

目录可能被另一个进程锁定或被设置为只读.
目录: ‘/Users/user-name/Library/Application Support/Autodesk/AutoCAD LT 2019 Standalone/R23.0/local/@zh_CN@’

错误原因

汉化补丁仅仅是对系统的界面进行了汉化,没有汉化AutoCAD启动向导页面的模板文件,因此/Users/user-name/Library/Application Support/Autodesk/AutoCAD LT 2019 Standalone/R23.0/local/目录下没有相关的简体中文的模板文件,CAD找不到目录,就认为CAD对 @zh_CN@文件夹 没有相关的权限,因此就报了一个只读的权限错误。

解决办法

这里只给命令行截面下的解决办法

# 1. 打开MAC的命令行工具
# 2. 定位到local语言目录下
cd /Users/user-name/Library/Application Support/Autodesk/AutoCAD LT 2019 Standalone/R23.0/local/
# 注意 /Users/user-name 中 user-name是你的用户名
# 3. 将英文版的向导模板文件拷贝为简体中文版
cp  -r \@en\@/  \@zh_CN\@
# 注意 这不是一种优雅的fix,真正的解决办法有待于官方release完整的中文语言包
# 本解决办法仅能解决安装简体中文版本后无法启动的问题
# 重新打开AutoCAD之后,中文截面就有了,就可以正常启动了。

从终端启动APP

open /Applications/App-name.app

设置MAC的Daemon启动项

Crontab在OSX上不是很好使,因为一旦重启的话,cron记录就会丢失,因此使用系统自带的Launchd Daemon来启动必要的服务。配置Launchd Daemon只需要一个plist文件,简单而且放便,适用于对系统已经安装过的服务设置自启动。OSX另外一种自启动的方式为StartupItems,这种方式不进需要使用plist来这设置,而且需要在plist的文件夹下指定可执行文件,比如一个脚本。因此Launchd Daemon是比较简单和方便的。配置Launchd是Mac OS下用于初始化系统环境的关键进程,它是内核装载成功之后在OS环境下启动的第一个进程。

以下以设置ngrok自启动为例

通常Launchd Daemon 放置在 /System/Library/LaunchAgents目录下

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>

    <key>Label</key>
    <string>com.localhost.ngrok.plist</string>

    <key>RunAtLoad</key>
    <true/>

    <key>StandardErrorPath</key>
    <string>/Users/nz/Documents/Crons/ngrok/ngrok-err.log</string>

    <key>StandardOutPath</key>
    <string>/Users/nz/Documents/Crons/ngrok/ngrok.log</string>

    <key>EnvironmentVariables</key>
    <dict>
      <key>PATH</key>
      <string><![CDATA[/usr/local/bin:/usr/bin:/bin]]></string>
    </dict>

    <key>WorkingDirectory</key>
    <string>/Users/nz/Documents/Crons</string>

    <key>ProgramArguments</key>
    <array>
      <string>/usr/local/bin/ngrok</string>
      <string>start</string>
      <string>ssh</string>
    </array>

  </dict>
</plist>

载入启动项

launchctl load /System/Library/LaunchAgents/com.localhost.ngrok.plist

取消启动项

launchctl unload /System/Library/LaunchAgents/com.localhost.ngrok.plist

如何不用任何第三方工具挂载系统的隐藏分区如EFI分区等

# 这里主要用到系统的命令行工具
# 使用以下命令打印系统已经识别的可以挂载的分区信息,如下面的列表显示,我的分区比较多可能列表有点长

sudo diskutil list

   #:                       TYPE NAME                    SIZE       IDENTIFIER
   # 0:      GUID_partition_scheme                        *120.0 GB   disk0
   # 1:                        EFI NO NAME                 314.6 MB   disk0s1
   # 2:         Microsoft Reserved                         314.6 MB   disk0s2
   # 3:       Microsoft Basic Data                         42.4 GB    disk0s3
   # 4:           Windows Recovery                         502.3 MB   disk0s4
   # 5:                 Apple_APFS Container disk1         76.5 GB    disk0s5

   # /dev/disk1 (synthesized):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   # 0:      APFS Container Scheme -                      +76.5 GB    disk1
   #                               Physical Store disk0s5
   # 1:                APFS Volume OSX                     51.9 GB    disk1s1
   # 2:                APFS Volume Preboot                 45.0 MB    disk1s2
   # 3:                APFS Volume Recovery                522.7 MB   disk1s3
   # 4:                APFS Volume VM                      3.2 GB     disk1s4

   # /dev/disk2 (internal, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   # 0:      GUID_partition_scheme                        *1.0 TB     disk2
   # 1:         Microsoft Reserved                         134.2 MB   disk2s1
   # 2:       Microsoft Basic Data Programs                85.9 GB    disk2s2
   # 3:       Microsoft Basic Data Documents               107.4 GB   disk2s3
   # 4:       Microsoft Basic Data Others                  214.7 GB   disk2s4
   # 5:                        EFI                         314.6 MB   disk2s5
   # 6:                  Apple_HFS osxhd1                  107.2 GB   disk2s6
   # 7:                  Apple_HFS osxhd2                  214.6 GB   disk2s7
   # 8:                  Apple_HFS osxhd3                  269.5 GB   disk2s8


# 使用 mount命令挂载你想挂载的分区,OSX默认的会挂载一般的分区,如APFS、NTFS文件系统的分区。默认的为了系统安全着想,EFI分区或是双系统Windows的DOS文件分区是隐藏的,如要查看这些隐藏分区的文件,就需要手动来挂载。这里我们以挂载EFI分区为例。

# 如要挂载EFI分区,这里 EFI 分区对应的就是标识符是 disk0s1
# 第一步 -> 新建一个目录作为磁盘的挂载位置
mkdir /Users/user-name/Documents/efi_temp
# 第二步 -> 挂载 EFI 分区到目录 /Volumes/efi
mount -t msdos /dev/disk0s1 /Users/user-name/Documents/efi_temp
# 第三步 -> 操作完成后取消挂载,防止误操作
diskutil unmount /dev/disk0s1

— 写在前

折腾这个的原因:

  1. 公司的没有钱部署自己的本地服务器,而只好用自己的办公电脑同时兼做服务器
  2. 网络为联通直连光纤,且联通线路维护人员拒不提供光猫的超级管理员密码,无法进入光猫设置光猫路由的lan端的端口映射
  3. 公司内部网络的布线决定了,无法将光猫运行模式改为PPPOE,且DDNS的方式也存在不稳定的情况
  4. 国内可以解决此问题的NAT123或是花生壳吃相太难看并且他们提供免费线路限流且极不稳定

使用NGROK解决此问题的前提

  1. 拥有公网IP的独立主机或是VPS
  2. 一定的Linux运维经验,能够熟练使用shell相关命令
  3. 熟悉linux的daemon运行模式,熟悉systemctl或是init.d的相关设置
# 注意我的服务器环境为Debian
# 建立编译操作的临时目录
mkdir ngrok_tmp
cd ngrok_tmp

# 配置和生成NGROK连接验证证书 #
openssl genrsa -out rootCA.key 2048
openssl req -x509 -new -nodes -key rootCA.key -subj "/CN=ngrok.domain.com" -days 5000 -out rootCA.pem 
openssl genrsa -out server.key 2048
openssl req -new -key server.key -subj "/CN=ngrok.domain.com" -out server.csr
openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out server.crt -days 5000

# 获取NGROK源码
git clone https://github.com/inconshreveable/ngrok.git

# 为NGROK编译指定证书文件,覆盖NGROK默认的证书文件
cd ngrok
yes | cp ../server.key ./assets/server/tls/snakeoil.key
yes | cp ../server.crt ./assets/server/tls/snakeoil.crt
yes | cp ../rootCA.pem ./assets/client/tls/ngrokroot.crt

make release-server

# 安装 NGROKD (Daemon) 到系统的常用路径PATH下
cp bin/ngrokd /usr/local/bin/

# 添加 NGROKD 服务项,以便自启动和使用service命令来启动和关闭
vim /etc/systemd/system/ngrokd.service


##################################
[Unit]
Description=ngrok service
After=network.target
After=syslog.target

[Service]
Type=simple
ExecStart=/usr/local/bin/ngrokd -domain=ngrok.domain.com -httpAddr 0.0.0.0:9280 -httpsAddr 0.0.0.0:9243  -tunnelAddr 0.0.0.0:9443 -log=/var/log/ngrokd.log -log-level=WARNING
ExecStop=/bin/kill $MAINPID
ExecReload=/bin/kill -USR1 $MAINPID
Restart=always

[Install]
WantedBy=multi-user.target
##################################
# 创建log日志文件,否则ngrokd无法启动
touch /var/log/ngrokd.log

# 将NGROD添加到系统自启动项
systemctl enable ngrokd

# 启动NGROD服务,或是使用service命令也可以
systemctl start ngrokd

# 查看NGROK服务端系统状态,或是使用service命令也可以
systemctl status ngrokd


# 编译客户端 
# 由于我的本地环境为iMAC,因此这里仅编译MAC环境下的客户端
GOOS=darwin GOARCH=amd64 make release-client

scp拷贝编译后的客户端到本地主机上


# 新建NGROK客户端配置文件,注意客户端配置文件为yml格式
vim .ngrok.yml 并写入以下配置项

#################################
server_addr: ngrok.domain.com:9443 # 这里指定客户端要连接的服务端的域名和端口号,4443是默认的监听的端口,可以在服务端进行更改
trust_host_root_certs: false # 这里指定是否验证服务端的证书,如果是从证书机构获取的证书则可以写为true,如果是自己生成的证书这里写为false
tunnels: # 这个配置项是你要开启的通道的配置
    ssh: # ssh是这个通道的名称,可以随意命名
        remote_port: 10039 # 该配置项可以申请远程服务器的固定端口,如果不设置该参数,每次在启动客户端的时候服务端都会随机分配一个端口号
        proto: # 该配置项指明该通道使用哪些协议
            tcp: 80 # 该配置项指明该通道开启本地tcp协议的80端口,即将远程的10039端口转发到本地80端口
#################################

# 创建 ngrok客户端日志文件,你可以根据自己的情况设置别的路径
touch /Users/user-name/Documents/Crons/ngrok.log


# 启动客户端,创建tunnel
/usr/local/bin/ngrok -log=/Users/user-name/Documents/Crons/ngrok.log -log-level=WARNING -config=/Users/nz/.ngrok.yml start-all


# 服务端运行如下提示
ngrok                                                                                                                                        (Ctrl+C to quit)

Tunnel Status                 online
Version                       1.7/1.7
Forwarding                    tcp://ngrok.domain.com:10039 -> 127.0.0.1:80
Web Interface                 127.0.0.1:4040
# Conn                        1
Avg Conn Time                 4821.21ms

# 以上输出证明端口映射成功,至此完结撒花

# 有时间考虑进一步研究以下,补齐添加额外tunnel和添加到mac自启动的相关操作

匹配文件中的邮箱地址

grep -Eiorh '([[:alnum:]_.-]+@[[:alnum:]_.-]+?.[[:alpha:].]{2,6})' [filename]  | sort | uniq

正则表达式语法参考链接

使用sed获取proxy中的ip和port

proxy样本的链接地址

这里的proxy很好,但问题是list中proxy的字段顺序并不是每一行都是一致的,这样的话正则表达式不是很好写

# 样本

{"type": "https", "host": "180.210.201.57", "export_address": ["180.210.201.57"], "response_time": 11.28, "country": "SG", "anonymity": "high_anonymous", "from": "proxylist", "port": 3130}

{"country": "US", "anonymity": "high_anonymous", "export_address": ["12.162.8.175"], "host": "12.162.8.175", "response_time": 6.42, "type": "https", "port": 80, "from": "txt"}

# sed命令
# 正则匹配的$1/$2/$3/$4,自由$2和$3是我们需要的
sed -r 's/(\{.*"host":\s")([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})".*"port":\s([0-9]{2,5})(.*\})/\2:\3/g'



# grep命令
# @TODO 需要进一步完善
grep -o  -E "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}"

网站添加文章基本都是由Markdown写成的,但是由于网站针对内容的排版有专门的定制,而markdown转换以后的html中有很多我不需要的东西,因此就需要将这部分不需要的东西清理掉。

不需要的html元素

  • 去除头尾的 artile 标签
  • a链接元素
  • 元素的Class、ID、Title等属性

正则替换



# 替换所有 a 链接标签 正则 <a\s.*?</a> # 替换所有ID / Class / Title属性 \s{0,1}(class|id|title)=".*?"

关于table 表格 转 html代码清洁化的问题

很多具有标准化产品的公司,产品信息很多时候都是存储在MS Excel表格之中,而当这些公司需要建站时,就需要将这些Excel的产品信息转化为单个网页页面放到网站上。很多人Dirty的做法是将Excel表格打开,然后将数据表格截图,以图片的方式上传到网站上。是的,很省事,但是搜索引擎是无法直接快速而准确的识别图片的内容的,对营销推广的工作不利。再者,如果客户对你的产品感兴趣,就不的不去手动打出来你产品的某个型号参数等信息,这样也给客户增加了麻烦。所以将MS Excel转HTML代码是必须要做的工作,不可图懒省事。那么问题来了,怎么做这件事?

其实转出来的最好结果是干干净净的HTML,只含有我们需要 table元素的基本标签比如table、thead、tbody、tr、td和包括colspan等基本属性的代码。至于class、id、fonts以及设计表格样式的inline css style完全是多余的,因为这些东西在我们设计网站的时候,已经预定义了。

比较简单的table我们可以用markdown来直接写,就是使用Dreamweaver或是其他可视化的HTML表格编辑工具来手动的敲一个个表格,但是如果一个表格几百行,这种效率显然是无法接受的。网上有很多EXCEL转HTML的网页工具,很多我都已经试过了,都存在各种各样的问题,比如转码之后表格布局改变了,或是误删除了空格表单。或是删不干净,最终还是留下我们很多代码中的脏东西。那么是不是还有其他方法,可以快速而高效的转换把MS EXCEL转 清洁的HTML?

答案是有的。大致的步骤:MS EXCEl 转存 htm格式,然后对 生成的代码进行替换。

这样做当然是有问题的,如果这样直接就能干,我就没有必要写下面的东西了。因为MS生成的htm格式文件的代码实在是太TM脏了,原谅我粗口,我是真的生气。MS不光加了一堆无用的格式信息,还添加了很多自定义的标签和属性,这真的是视W3C的标准不存在,对于这种没有节操又任性的公司,直接放弃,转向另外一种方式。既然魏然不支持W3C标准,那么我们就找支持W3C标准的办公软件,尤其是有表格SpreadSheet功能Office软件。你脑袋一亮,对确实有一家这样的公司,他就是Apache基金会,他除了有Apache这样享誉全球的服务器软件,还有大数据处理的系统工具Spark,当然还有我们今天的主角 Apache OpenOffice。Apache基金会维护了350+大型而健壮的软件项目,而且这一数量还在不断上升,而这些大型的软件绝大部分都是开源的,意味着他们都是免费的。So BIG DISS to MS.

以下是OpenOffice下的调整好的表格基本样式。

OpenOffice表格

直接将编辑好的表格另存为html,然后你就会惊喜的发现生成的代码非常规范和美观。

完全没有乱七八糟的东西,我们只需要删除代码中的frame、rule、border、fonts 信息就可以直接Copy到网站上了。

当然如果你对字体这块没要求的话,不删除就可以直接放到网站上。

# 正则替换 table 标签多余元素
\s{0,1}(frame|rules|border)=".*?"

# 正则替换字体信息
<font.*?>|</font>

最终的结果就是这样

完美撒花。

@TODO 后期考虑专门针对这种情况做一个小软件

工作上我需要建立一个新的adwords账户,由于账户设置的复杂性,我需要使用Google Adwords Editor来设置结构化的广告Campaign,之所以不用adwords的web界面是因为效率太低了。

然而最新版适用于Mac OSX EI Capitan 10.11的Google Adwords Editor 版本是 12.2.3, 如果你碰巧手贱跟我一样更新了他,抱歉你就再也回不去了,因为从12.2.3版本以上就不再支持EI Capitan版本,因此你需要Rollback。如果你回到Google的官网寻找历史版本,不好意思,没有相关的链接指向历史版本的下载地址。我第一次觉得Google如此脑残,历史版本不放出来,因此忍不住就在Google Adwords的Community上开帖吐槽, 最后Google内部的Adwords工程师Igor贴出了12.2.3的下载链接。如果你和我遇到了同样的问题,你可以在这里下载。

Rollback之后,遇到了另一个问题。以下Google Adwords Editor简称GADE。这个版本的GADE不支持Socks代理,仅支持http代理,而我这里的情况是https代理极不稳定,而留给我的选择就只剩下VPN。而目前市场上活着的VPN也是各种不稳定,所以我决定自己搭建VPN,最终了Wireguard解决方案。

远端的服务器环境是Debian 9 Stretch,可以按照官方文档apt-get直接安装。Wireguard主要有wireguard-dkms和wireguards-tools两个组件,而这两个组件依赖linux-header来运作,而安装当前系统内核相配套的linux-header则需要安装GCC6.0以上的版本。如果你的debian的GCC版本为4.0,而apt安装最新的gcc报了一堆依赖‘you have held broken packages.’不满足条件的错误,你需要检查一下你的distribution的具体版本。

# gcc版本4.0并且报依赖错误
# 先检查服务器版本,两个方法
# LSB - Linux Standard Base
lsb_release -da

No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 9.5 (stretch)
Release:    9.5
Codename:   stretch

# 或是读取etc目录下的系统版本os-release配置文件

cat /etc/os-release

PRETTY_NAME="Debian GNU/Linux 9 (stretch)"
NAME="Debian GNU/Linux"
VERSION_ID="9"
VERSION="9 (stretch)"
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/


# 这里出现依赖错误的原因是,我在更新vps的时候,误将jessie更新到了stretch,而source.list.d依然使用的是jessie版本的配置文件,自然依赖就报错了。
# 在更新了source.list和source.list.d的配置,update之后,依赖的问题解决。

# 这里贴一下stretch的Debian Official Reposity,以便以后使用
deb http://deb.debian.org/debian stretch main contrib non-free
deb-src http://deb.debian.org/debian stretch main contrib non-free

deb http://deb.debian.org/debian-security/ stretch/updates main contrib non-free
deb-src http://deb.debian.org/debian-security/ stretch/updates main contrib non-free

deb http://deb.debian.org/debian stretch-updates main contrib non-free
deb-src http://deb.debian.org/debian stretch-updates main contrib non-free

# wireguards 是基于Linux内核的,所以依赖于linux-kernel-header 和 kernel模块。
# header 模块的正常编译和加载并不一定要求header版本和kernel版本一致,但是如果kernel header版本太老,
# 一些新的kernel header的特性就无法在编译是被应用
# 这里,我们建议将linux-kernel-header 是跟当前系统内核版本保持一致,这样就会少出问题
# 注意 uname -a 检查的只是内核的版本
# uname -r 返回结果含有内核的版本号和发行版的版本号
# 由于Linode vps的特殊性,在安装完新的kernel和header之后,你需要检查vps的profile是否是grub2引导方式
# 如果不是,需要在console页面修改,修改完之后需要在terminal界面运行 update-grub 命令来进行更新引导加载

# 安装linux header
# Debian 默认的kernel header安装目录为/usr/src

apt install linux-headers-$(uname -r)


# 安装 wireguard
apt-get install wireguard

# 注意安装和配置wireguard需要root权限

...
wireguard:
Running module version sanity check.
 - Original module
   - No original module exists within this kernel
 - Installation
   - Installing to /lib/modules/4.9.0-3-amd64/updates/dkms/

depmod...

DKMS: install completed.

# wireguard 安装时,会根据当前的header来编译内核模块
# depmod -> 生成模块
# 编译后的内核模块位置为 /lib/modules/4.9.0-3-amd64/updates/dkms/wireguard.ko

# 检查wireguard模块是否被安装
dkms status

# 检查wireguard内核模块是否加载
# 如果没有返回任何结果,则表示模块没有被自动加载,那么wireguard就无法正常运行
lsmod | grep wireguard

# 加载激活 wireguard 内核模块
modprobe wireguard


# 配置 wireguard

# Wireguard是双向对等的VPN,这里为了方便理解,我就称公网服务器为服务端,自己的电脑为客户端

# 生成服务端公钥和私钥
# 分别会在当前目录生成 publickey 和 privatekey 文件
wg genkey | tee privatekey | wg pubkey > publickey

# 注意: 如果你不小心多次生成了密钥,而不是到当前服务端或是客户端使用的到底是哪个密钥,可以通过先启动 wireguard 服务, 使用 wg showconf 命令来打印 密钥的相关信息

# 配置/etc/wireguard/wg0.conf 文件

touch wg0.conf
vi wg0.conf


----------------------------------------------------------
[Interface]
# 服务端在VPN内网IP地址
Address = 192.168.9.1/24
# 如果动态添加客户端或是peer的信息,这里会自动保存
SaveConfig = true
# 添加流量转发的防火墙规则,如wireguard激活则转发生效,不激活则删除规则
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
# 服务端监听节点
ListenPort = (端口号)
# 服务端私钥
PrivateKey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

[Peer]
# 客户端的公钥,针对我的情况,就是Mac主机上生成的公钥
PublicKey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# 客户端接入时,客户端在VPN内网的IP地址
AllowedIPs = 192.168.9.101/24
----------------------------------------------------------

# 更方便的方法是,不将客户端的信息写在配置文件中,采用命令的方式添加客户端信息
# 在公网服务器运行一下命令
# 由于我们设置了SaveConfig = true,因此一下命令运行后,peer信息会自动添加到wg0.conf中
wg set wg0 peer [客户端公钥] allowed-ips 192.168.9.101/24



# 激活 wireguard 服务

wg-quick up wg0


# 如果出现错误提示 resolvconf: command not found
# 则说明 resolvconf 没有安装,直接apt安装即可
# 之后重新运行命令即可激活wireguard服务,成功运行提如下
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip address add 192.168.9.1/24 dev wg0
[#] ip link set mtu 1420 dev wg0
[#] ip link set wg0 up
[#] resolvconf -a tun.wg0 -m 0 -x

# 使用 ifconfig 命令查看wireguard的wg0网卡状态,确认一切正常

ifconfig

wg0: flags=209<UP,POINTOPOINT,RUNNING,NOARP>  mtu 1420
        inet 192.168.9.1  netmask 255.255.255.0  destination 192.168.9.1
        unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  txqueuelen 1  (UNSPEC)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

# 为避免由于服务器启动而造成wireguard终端,可以将wireguard添加为系统启动项
systemctl enable wg-quick@wg0

> Created symlink /etc/systemd/system/multi-user.target.wants/wg-quick@wg0.service → /lib/systemd/system/wg-quick@.service.

# 从系统控制台启动wireguard的命令为

systemctl start wg-quick@wg0

> wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0
>   Loaded: loaded (/lib/systemd/system/wg-quick@.service; enabled; vendor preset: enabled)
>   Active: active (exited) since ......


# 客户端安装 #
# 我的环境是 Mac,其他环境请参照官方文档 #

brew install wireguard-tools

# 配置wireguard客户端 #


# 新建配置文件

sudo mkdir /etc/wireguard && cd /etc/wireguard
sudo touch wg-mac.conf
sudo vi wg-mac.conf

----------------------------------------------------------
[Interface]
# 客户端生成的私钥
PrivateKey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# 客户端在VPN内网的地址
Address = 192.168.9.101/24
DNS = 8.8.8.8

[Peer]
# 服务端的公钥
PublicKey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# 服务端的域名:端口或是IP:端口
Endpoint = domain.com:10311
AllowedIPs = 0.0.0.0/0
----------------------------------------------------------

# 在Mac上启动wireguard客户端和运行服务端一样,同样使用wg-quick命令
# 其中 up 后面跟的参数就是刚刚的配置文件名去掉conf后缀
sudo wg-quick up wg-mac

> Warning: `/private/etc/wireguard/wg-mac.conf' is world accessible
> [#] wireguard-go utun
> WARNING WARNING WARNING WARNING WARNING WARNING WARNING
> W                                                     G
> W   This is alpha software. It will very likely not   G
> W   do what it is supposed to do, and things may go   G
> W   horribly wrong. You have been warned. Proceed     G
> W   at your own risk.                                 G
> W                                                     G
> WARNING WARNING WARNING WARNING WARNING WARNING WARNING
> INFO: (utun0) 2018/09/05 17:02:36 Starting wireguard-go version 0.0.20180613
> [+] Interface for wg-mac is utun0
> [#] wg setconf utun0 /dev/fd/63
> [#] ifconfig utun0 inet 192.168.9.101/24 192.168.9.101 alias
> [#] ifconfig utun0 up
> [#] route -q -n add -inet 0.0.0.0/1 -interface utun0
> [#] route -q -n add -inet 128.0.0.0/1 -interface utun0
> [#] route -q -n add -inet xxx.xxx.xxx.xxx -gateway 192.168.1.1
> [#] networksetup -getdnsservers Bluetooth PAN
> [#] networksetup -getdnsservers Wi-Fi
> [#] networksetup -getdnsservers Thunderbolt Bridge
> [#] networksetup -setdnsservers Thunderbolt Bridge 8.8.8.8
> [#] networksetup -setdnsservers Wi-Fi 8.8.8.8
> [#] networksetup -setdnsservers Bluetooth PAN 8.8.8.8
> [+] Backgrounding route monitor

# 关闭Wireguard
sudo wg-quick down wg-mac

# 测试链接是否正常可以使用ping
ping 192.168.9.1

# 或是通过监控流量方式来测试
 ifstat -i utun0

 > utun0
 > KB/s in  KB/s out
 >   0.00      0.14
 >   0.44      0.13
 >   0.00      0.04
 >   0.00      0.05
 >   0.00      0.04
 >   0.00      0.04
 >   0.04      0.00
 >   0.00      0.00
 >   0.00      6.70
 >   5.73      0.61
 >   5.24      0.74
 >   0.89      1.38


免费的wireguard线路

mosh因为G/F/W被封之后的 ssh 连接服务器的解决办法


ssh -o ProxyCommand='nc -x 127.0.0.1:7081 %h %p' user@domain.com # 本地shell客户端的˙配置 # keepalive or autossh? 需要进一步测试 # 客户端针对不同主机的设置 # 针对断线的问题 # 每30秒向服务器发送在线状态提示 ServerAliveInterval 30 # 一次连续的在线状态提示发送次数,240次X30秒=7200秒=2小时 ServerAliveCountMax 240

关于netcat配合socks proxy连ssh的相关信息可以(参照链接)[https://ieevee.com/tech/2017/10/19/ssh-over-socks5.html]

关于发现rpcbind运行在底层并监听大量端口的情况

了解到当用户使用NFS系统来共享文件时,Debian发行版会默认的运行rpcbind和rpc.statd两个服务。而这不是我们所需要的,而开启的端口会降低服务器的安全性,因此需要将rpcbind服务关闭。

这里需要注意的是,rpcbind由systemctl来管理,而不是我们常用的service命令。

systemctl stop rpcbind.service
systemctl disable rpcbind.service

Chrome和Firefox针对特定的DOM对象,同样的JQ代码,执行的结果可能不一样。

例如: 针对img元素,获取元素的width、height,结果其实是不一样的。

// 获取img元素的宽度
$('img').width();

/*
 * 在chrome中获取的width是图片的可视宽度,而非图片的netural宽度(真实宽度)
 * 在firefox中获取的width是图片的真实宽度而是可视宽度
 * 以上行为的差距就使得在通过获取width来给特定dom元素定位是,
 * 不同的浏览器显示的效果就会不一致
 * 针对以上这种情况需要是用JS的原生方法来获取元素的宽度
 */

//使用querySelectAll获取dom元素,绝大部分的主流浏览器已经支持该方法
Img = document.querySelectorAll('img')[0];
//获取Img对象的可视宽度
visualWidth = Img.clientWidth;
//获取Img的真实宽度
neturalWidth = Img.naturalHeight;


/*
 * 除了以上的问题之外,firefox对js代码的执行会非常快,例如针对img图片元素绑定事件,
 * 如果不检查图片是否完全载入而直接执行代码的话,代码是无法正常运行的,
 * 这一点chrome似乎没有这个问题。
 */

//JQ监听判断图片是否完全载入

$(img).load(function(){
    // Do something.
})


LAMP安装与配置

前期安装

  1. 更新 source list

# 这里我主要用的是linux - Debian的发行版,因此Reposity都是debian官方的。 # 尽量不要使用VPS服务商提供的Reposity,虽然更新可以省流量, # 但是有时候有的软件包可能被修改,或是功能被阉割,部署到生产服务器有时候会出现莫名其妙的问题 # 因此还是建议使用官方的Reposity ###### Debian Main Repos ###### deb http://ftp.us.debian.org/debian/ jessie main contrib non-free deb-src http://ftp.us.debian.org/debian/ jessie main contrib non-free ###### Debian Security Repos ###### deb http://security.debian.org/ jessie/updates main contrib non-free deb-src http://security.debian.org/ jessie/updates main contrib non-free ###### Debian Backports Repos ###### deb http://ftp.debian.org/debian jessie-backports main
  1. 安装mosh
  2. 安装git, fail2ban
  3. 安装curl
  4. 设置系统时间与字符编码UTF-8

@TODO 以上几个重要的软件包有时间再写

LAMP

  1. 安装apache2
apt-get install apache2 
#由于我们需要使用clean url,因此需要激活apache的rewrite模块
a2enmod rewrite
  1. 安装mariadb – mysql
apt-get install mariadb-server mariadb-client
# 运行mysql_secure_installation 提升数据库初始化的安装性
mysql_secure_installation


apt-get install mariadb-server mariadb-client

#运行mysql_secure_installation 提升数据库初始化的安全性
mysql_secure_installation

#建立mysql用户
create user 'user'@'localhost' identified by 'passwd';

#配置数据库用户权限
GRANT ALL PRIVILEGES ON dbname.* TO 'user'@'localhost' WITH GRANT OPTION;
# 注意,这里和mysql可能有一点不同,必须加上 WITH GRANT OPTION,否则 用户添加的权限一直都是USAGE,USAGE不是实际的权限,因此是无效的,

# 修改数据库用户密码
SET PASSWORD FOR 'user'@'localhost' = PASSWORD('passwd');
# 这里和mysql也不一样,mysql修改用户密码使用的alter语句,这里也需要注意。

# 查看用户权限
SHOW GRANTS FOR 'username'@'localhost';

# 刷新权限
FLUSH PRIVILEGES;

  1. 安装php5 (debian8目前stable repository中还没有加入php7)

apt-get install php5 php5-curl php5-gd php5-mysql php5-xmlrpc php5-json

  1. 服务器安全性提升(安全性优化)

@TODO这里暂时略过,有时间再写

  1. 建立网站文件目录

mkdir -p /var/www/domain.com/html/

  1. 安装certbot证书签发程序

# 安装基本库 apt-get install gcc build-essential autoconf openssl # 下载certbot最新版客户端 git clone https://github.com/certbot/certbot.git # 解压后,运行目录下的certbot-auto安装certbot客户端 ./cerbot/certbot-auto --install-only # 使用最新版本的ACME2特性签发wildcard域名证书 # 注意wildcard证书只针对子域名,而根域名也需要签发证书,所以以下包含两个域名 ./certbot-auto --server https://acme-v02.api.letsencrypt.org/directory -d *.domainexample.com -d domainexample.com --manual --preferred-challenges dns-01 certonly # 在证书签发的过程中需要添加DNS TXT解析记录,已验证域名所有权,这里需要注意 # 签发成功后出现以下提示 - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/domainexample.com/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/domainexample.com/privkey.pem Your cert will expire on 2018-07-01. To obtain a new or tweaked version of this certificate in the future, simply run certbot-auto again. To non-interactively renew *all* of your certificates, run "certbot-auto renew" # 如果因为一些原因而错误签发了证书,可以使用revoke命令撤销 # 如果你使用的是cert-only子命令获取的证书,这需要使用证书文件和私钥来撤销证书 ./certbot-auto revoke --cert-path /etc/letsencrypt/archive/domainexample.com/cert1.pem --key-path /etc/letsencrypt/archive/domainexample.com/privkey1.pem

2018年06月26日补充内容

由于LE的证书有效期只有三个月,因此不到三个月就要对证书进行renew,这就是免费证书所需要付出的代价,好在FE证书支持全站子域名的wildcard证书,三个月renew一次也就忍忍吧,不服和图方便的话,一年花个100刀买证书吧,毕竟钱花哪哪好。

由于目前我使用的就是wildcard证书,为wildcard证书需要连接acme-v02进行验证,并且wildcard目前只支持dns-01的授权方式,因此就不得不每次手动的manual来进行证书的renew。Renew的方法不是用certbot-auto的renew命令,因为是手动的,就需要使用certonly命令。后期考虑结合VPS服务商的DNS API来自动解决这个问题。

./certbot-auto \
--server https://acme-v02.api.letsencrypt.org/directory \
-d *.domainexample.com \
-d domainexample.com \
--manual \
--preferred-challenges dns-01 \
certonly


# 命令运行后会提示添加 TXT 文本格式的 DNS解析记录
# 注意由于添加证书的域名是根域名+wildcard子域名,因此需要添加2条DNS记录。
# 不然的话一直会验证不通过。

# 验证通过后顺利生成新的证书,并且/etc/letsencrypt/live/domainexample.com/目录下的证书指向软连接已经被成功更新。
# 重启或是reload apache服务,就会发现证书顺利的rewnew和更新了。

# service apache2 restart 或者 service apache2 reload

  1. clone与修改apache的配置文件

# 443 安全连接与端口配置文件 # 注意事项 # 1. ssl的配置文件名和ssl的配置文件选项 # 根域名跳转到www <VirtualHost *:443> ServerName domainexample.com Redirect permanent / https://www.domainexample.com/ # 即使根域名全站跳转,也必须要明确的支出证书文件的路径,否则就会出现文章底部的错误。 SSLEngine on SSLCertificateFile /etc/letsencrypt/live/domainexample.com/cert.pem SSLCertificateKeyFile /etc/letsencrypt/live/domainexample.com/privkey.pem SSLCertificateChainFile /etc/letsencrypt/live/domainexample.com/fullchain.pem </VirtualHost> <VirtualHost *:443> ServerAdmin admin@domainexample.com ServerName www.domainexample.com DocumentRoot /var/www/html/domainexample.com/public_html/ ErrorLog /var/www/html/domainexample.com/logs/error.log CustomLog /var/www/html/domainexample.com/logs/access.log combined SSLEngine on SSLCertificateFile /etc/letsencrypt/live/domainexample.com/cert.pem SSLCertificateKeyFile /etc/letsencrypt/live/domainexample.com/privkey.pem SSLCertificateChainFile /etc/letsencrypt/live/domainexample.com/fullchain.pem </VirtualHost> # 2. 80端口的配置文件 # Redirect all 80 port traffic to 443 instead <VirtualHost *:80> ServerAdmin admin@domainexample.com ServerName domainexample.com ServerAlias www.domainexample.com DocumentRoot /var/www/html/domainexample.com/public_html/ ErrorLog /var/www/html/domainexample.com/logs/error.log CustomLog /var/www/html/domainexample.com/logs/access.log combined <IfModule mod_rewrite.c> RewriteEngine on RewriteCond %{SERVER_PORT} !^443$ RewriteRule (.*) https://%{SERVER_NAME}/$1 [R] </IfModule> </VirtualHost> # 上面的这种操作是一种heavy lift 如果需要跳转的链接成千上万,服务器就会有很重的负载 # 针对80端口的流量跳转 # 使用virtualhost的redirect属性设置比较优雅和高效 # 将所有根域名下的流量跳转到www <VirtualHost *:80> ServerName domainexample.com Redirect permanent / https://www.domainexample.com/ </VirtualHost> # 另外还需要在ServerName domainexample.com <VirtualHost *:80> ServerName www.domainexample.com # 由于domainexample.com已经被设置为一个独立的virtual host # 因此下方的ServerAlias设置项必须删除,这里我们直接注释掉 # ServerAlias domainexample.com ServerAdmin admin@domainexample.com DocumentRoot /var/www/html/domainexample.com/public_html # 将所有80端口流量重定向到443端口 Redirect permanent / https://www.domainexample.com/ DocumentRoot /var/www/html/domainexample.com/public_html/ ErrorLog /var/www/html/domainexample.com/logs/error.log CustomLog /var/www/html/domainexample.com/logs/access.log combined </VirtualHost> # 3. 激活Apache的ssl模块 a2enmod ssl # 这样与ssl相关的依赖模块也会被激活 # 之后重启服务器就可以愉快的享受ssl带来的安全性了。 service apache2 restart

# @TODO 以下有时间再补 # Apache配置与服务器性能优化 # 1. 提升安全性 # 2. 开启流量压缩 # 3. 并行下载,静态资源分离 # 4. 禁用不安全的HTTP协议

企业邮箱配置

企业邮箱的话,如果你的公司有营业执照和公章,则可以通过和阿里的OA平台 – 钉钉 签约,然后使用阿里云提供的免费企业邮箱服务,50个账户,每个账户5G容量,对于人数不多的公司完全够用了,一年可以省下将近2000块钱的费用。按照指引注册的话,快的话一天就可以审核通过使用了。

注意这里面也是有坑的。

  1. 你的域名无法直接绑定到钉钉提供的免费企业邮箱,必须按照钉钉的提示先申请一个 company-name.onaliyun.com的二级域名,然后企业邮箱的功能算是激活了。
  2. 进入钉钉手机端,钉邮 -> 【邮箱管理】-> 【绑定新域名】-> 添加自己公式的域名,然后后台会提示需要你添加DNS解析记录以验证域名所有权。
  3. 第二步是在域名的DNS解析页面上设置mx解析记录,解析到mxw.hichina.com和mxn.hichina.com,priority优先级分别是5和10。这两个服务器是阿里企业邮箱的DNS服务器。除此之外还要添加一个CNAME的DNS记录,主机为mail,地址为mail.mxhichina.com。由于MX记录同步比较慢,通常需要1-2个小时才会生效。
  4. 验证通过后自己的域名就会现在在邮箱的管理页面中,在钉钉手机端后台选中自己的域名设置为默认,这样所有人的企业邮箱都会变成自己域名的后缀。
  5. 走完这一步,就可以在钉钉的通讯录中邀请公司的同事加入组织,并为他们配置企业邮箱账户。
  6. 然而即使你为每个人都配置完企业邮箱了,这个时候你仍然没有办法使用客户端来收信。什么?你就是为了骗企业邮箱和邮件客户端收信这个来的?别急。即使你用foxmail添加了邮箱的地址,你输入了钉钉的账户密码,IMAP/SMTP端口什么的你都配置正确,你还是收不到信。那么怎么办?你需要在钉钉的后台 【通讯录】->【邮箱账户管理】->【个人邮箱】->【重设密码】为每个邮箱账户重设密码,然后每个人都登陆一下阿里企业邮箱网页版 https://qiye.aliyun.com 修改密码。因为企业邮箱的登录密码,不是钉钉账户的密码。所以用钉钉账户的密码是没有办法在邮箱客户端登陆邮件服务器的。改完密码之后使用新密码配置邮件客户端,一切都正常了。而搞笑的是,打电话给钉钉的客服,客户告诉我免费账户不支持IMAP客户端收信,嗯嗯,你说的对,但是我不信。

2018年7月4日补充更新

当你打开网站的https页面出现错误SSL_ERROR_RX_RECORD_TOO_LONG(firefox)或是发送的响应无效 ERR_SSL_PROTOCOL_ERROR (Chrome)时, 且测试 http://www.domain.com:443 可以访问,说明http运行在443端口。

且你确信证书的签发没有问题,且netstat -npl 列出的端口也是开放的,因此出问题的就是apache2的配置文件,准确的说应该是site-available目录下的配置文件有问题。

注意无论是哪个server name下来的站点配置文件,即使全站跳转,配置文件也必须明确的指除证书文件的具体路径,否则就会出现上面的问题。解决办法就是在每一个virtualhost *:443 配置项下都必须明确指出SSL证书相关文件的路径。

配置项的问题解决之后,service apache2 reload 一切就正常了。

Drupal Node View Mode

Drupal针对node和entity设置有不同的view mode,不同的view mode可以有不同的展示方式。

Node和entity可以根据需要添加不同的字段,而每个字段可以在manage display选项卡下进行相关的设置。

当在manage display中将字段下拉到hidden中时,前台的模板tpl文件则获取不到相关的字段信息,即被设置为hidden的字段是没有被core process的。

针对多级产品分类与产品展示的相关问题的解决办法

假如网站的层级分类如下

term1
|—-term a
|—-term b
term2
|—-term c
term3
|—-term d
|——–term (1)
|——–term (2)
|——–term (3)
|——–term (4)

由于drupal默认的分类系统采用,如果该分类下无 taged node, 则该分类taxonomy term会被显示为 no content,和实际的业务需求脱节。正常的,一个网站会有多级分类系统,分类下如果有子分类,则在上一级的父分类页面应该显示属于该分类下的子分类。比如term 3页面应该显示其下的子分类 term d, term (1), term (2), term(3), term (4)。最好的情况是term d的页面需要显示被其下子分类 taged node。

要实现这样的功能,就不得不利用到view mode和views 模块。

drupal 默认的分类展示页面是将term下的taged node依次用node.tpl.php在view mode -> teaser 下渲染出来。这种显示方式缺乏灵活性,我们将会使用views -> block来替换装这种处理方式。

具体的处理办法是:

  1. 修改node.tpl.php模板文件,删除 view mode -> teaser 下模板打印的所有内容
  2. 回到node field管理页面,在manage dispaly 选项中将 teaser 模式下的所有字段设置为hidden。
  3. 新建views – nodes list block, 标的为node field, context filter 中添加字段 has taxonomy term with depth, depth可以根据需要设置为2-5,如果分类层级比较多,则可以将depth设置的大一些。
  4. 使用hook_page_build覆写taxonomy term页面的renderable array, 使用views_embed_view将新建的views – nodes list block添加到 $page中。剩下的就是将block交给drupal的主题层渲染了。

这里需要注意的是:

  1. $page[‘content’][‘system_main’][‘term_heading’]有些多余,应该unset
  2. 如果有的分类下没有taged node,则需要在hook_page_build中将$page[‘content’][‘system_main’][‘no_content’])进行unset

正版key拿走不谢,windows和mac版都可以正常激活,9.0及以上版本

Username License Key
G.N.R.S.U 327B0AA4F8-1554383598-2CFFC4975F
SoleWe.com F257BC47D3-1554383604-CC36A5946F
SoulCourier.com 2C7A3D0B2C-1554383610-5598CFD6A8
YaWego.com 5CC5DAFBCA-1554383615-89B2F1629C

Form API

Form元素由指定格式的数组构造,然后由渲染引擎渲染出来,这里主要说一下比较特殊的一些表单元素

  • tableselect

多选的表格表单的主要结构如下:

$form['inquiry_category'] = array(
    'tableselect' => array(
        '#type' => 'tableselect',
        // 表格的表头,包含的field1、field2、field3、field4分别表示一个单元格
        // 每一个单元的array的data键的值,为具体该单元格的内容
        '#header' => array(
            'field1' => array(
                'data' => '头部表格的具体内容1'
            ),
            'field2' => array(
                'data' => '头部表格的具体内容2'
            ),
            'field3' => array(
                'data' => '头部表格的具体内容3'
            ),
            'fiel4' => array(
                'data' => '头部表格的具体内容4'
            ),
        ),
        // 表格的body部分
        '#options' => array(
            // 在默认的情况下,checkbox的值为0-n的index数值
            // 当数据展示的内容为数据库表,且含有id字段时
            // 由于数据库的id并不以0开头,且id可能并不连续
            // 因此在多选表格在提交之后,提交的表单值$form_states['values']并不能确定到底是哪一个ID的结果被选中
            // 所以这里就需要将#options的每一个子数组的键统一设置为id的值,避免使用0-n的顺序下标从而导致表单提交后数据库操作缺失标的而带来的一些列问题
            // 如下面的例子,#options的第一个子元素为一条数据库记录, drupal默认的row1_id为0,这里我们需要将它替换成数据库中的主键,这样表单在提交之后,主键的值就会包含在$form_states['values']中,下一步就可以遍历,从而更新数据库的记录。
            'row1_id' => array(
                'field1' => array(
                    'data' => 'row1-字段1的具体内容'
                ),
                'field2' => array(
                    'data' => 'row1-字段2的具体内容'
                ),
                'field3' => array(
                    'data' => 'row1-字段3的具体内容'
                ),
                'fiel4' => array(
                    'data' => 'row1-字段4的具体内容'
                ),
            ),
        ),
        '#empty' => 'No records!',
    ),
    // 当有时候需要向表单的submit函数传递表单或是页面的参数时
    // 比较方便的方法为使用hidden隐藏字段来传至,如果为前台页面,隐藏字段的值需要hash,如果是后台页面,就可以省事一点。
    // 比如,我需要知道这张table展示的具体是哪个数据库表的记录,而数据库表有多个,数据库表的名字是个变量$table_name
    // 那么就可以用$table_name的值新建一个隐藏字段,从而将$table_name的值添加到$form_state['values']中,从而方便后面的数据库操作
    'table_name' => array(
      '#type' => 'hidden',
      '#value' => $table_name,
    ),
    // drupal表单的提交元素中,actions是一个wrap元素,主要用来wrap一组提交动作
    // 具体的提交动作,可以分为 submit、button、image_button
    // 其中submit为默认的表单提交原始。
    // 如果定义表单的函数名为module_name_function_name_form,其中module_name为模块名,function_name为功能或是实现目的的名字,form为后缀
    // 那么默认的情况下,该表单的提交函数为module_name_function_name_form_sumbit,即在表单构造函数的末尾加上'_submit'后缀,这是drupal规定的一种规范。
    // 同理,在表单构造函数的末尾加上后缀'_validate', 则是该表单的提交结果验证函数,主要验证表单输入的数据是否合法
    // button或是image_button则需要制定相关的提交函数,否则button在被点击之后不会执行相关的提交操作
    // 这里需要注意的是 button元素的默认属性'#executes_submit_callback'默认为FALSE,即提交函数不会被执行,因此针对button或是image_button元素,还需要将
    // '#executes_submit_callback'的值设置为TRUE,这样提交函数才会被调用
    // form actions还可以直接是#markup, #markup为一段html代码,多为链接,比如cancel操作,就是返回之前的页面
    // 其中l()为链接构造函数,t()文本清洁函数
    'actions' = array(
        'mark' => array(
            '#type' => 'button',
            '#value' => t('标记为已读'),
            '#submit' => array('inquiry_category_tableselect_status_read'),
            '#executes_submit_callback' => TRUE,
        ),
        'delete' => array(
            '#type' => 'button',
            '#value' => t('删除询盘'),
            '#submit' => array('inquiry_category_tableselect_delete'),
            '#executes_submit_callback' => TRUE,
        ),
        'cancel' => array(
            '#markup' => l(t('Cancel'), 'cancel/link/localtion'),
        ),
    ),
);

Form API – Ajax form

关于表单提交之后的操作

https://drupal.stackexchange.com/questions/37719/do-something-after-submit-the-form-at-hook-form-alter

token和form api的结合使用
https://clikfocus.com/blog/how-to-add-token-support-for-drupal-7

自定义表单的函数渲染以及与materialize主题集成
#theme
#theme-wrap

关于hook_menu

‘page arguments’ 可以接受路径为参数,但参数的格式必须为array()
例如定义一个路径 admin/manage/products/type1/12

** 路径到参数数组的转化是从0开始的 **

admin -> 0
manage -> 1
products -> 2
type1 -> 3
12 -> 4

如果需要最后一位12传递到 ‘page arguments’ 为 ‘page_callback’ 的参数
则可以这样写 ‘page arguments’ => array(4)

Renderable Array

构建renderable array之后,返回这个array作为page callback函数的结果
drupal就会自动调用系统的主题层,对array进行渲染,将array转化为html或是json数据

默认有两种组织方式

  1. 使用theme_element函数来返回回调结果,具体参照drupal api文档
    常见的有 theme_image() theme_link() theme_image_style() theme_table() theme_item_list()等等
    这些元素的多使用hook_elements()函数来进行定义。

  2. 直接返回一个数组

    直接返回一个array(),由drupal核心来选择调用哪些方法来渲染出整个array
    这里需要注意的是,返回的array必须符合相关的要求。
    根据drupal官方文档的说明,以返回table表格元素和item-list列表元素为例

    Renderable array的优势在于可以添加很多属性,调整渲染后html的结构

// 假如使用第一种方法 
$html_table_output = theme_table(
    array(
        'header' => array(
            array('data'=>'ID'),
            array('data'=>'Name'),
            array('data'=>'Email'),
            array('data'=>'Status'),
        ),
        'rows' => array(
            //row1
            array('data' => array(
                array('data' => 'row1-col1-data'),
                array('data' => 'row1-col2-data'),
                array('data' => 'row1-col3-data'),
                array('data' => 'row1-col4-data')
          )),
            //row2
            array('data' => array(
                array('data' => 'row2-col1-data'),
                array('data' => 'row2-col2-data'),
                array('data' => 'row2-col3-data'),
                array('data' => 'row2-col4-data')
          )),
        ),
    );
);

$html_table_renderable_array = array(
    '#theme' => 'table',
    '#prefix' => '<div class='class-name'>',
    '#suffix' => '</div>',
    '#attributes' => array(
        'id' => 'some-id',
        'calss' => array(
            'class-a',
            'class-b'
        ),
    ),
    '#header' => array(
            array('data'=>'ID'),
            array('data'=>'Name'),
            array('data'=>'Email'),
            array('data'=>'Status'),
        ),
    '#rows' => array(
        //row1
        array('data' => array(
            array('data' => 'row1-col1-data'),
            array('data' => 'row1-col2-data'),
            array('data' => 'row1-col3-data'),
            array('data' => 'row1-col4-data'),
         )),
        //row2
        array('data' => array(
            array('data' => 'row2-col1-data'),
            array('data' => 'row2-col2-data'),
            array('data' => 'row2-col3-data'),
            array('data' => 'row2-col4-data'),
        )),
);


$html_item_list_output = theme_item_list(
    array(
        'items' => array(
            //如果item是字符串,这字符串就是每一个li的内容
            //如果item是array,这array的data键的值,是li的内容,其他的键则作为li标签的属性
            //这里需要注意的是class的值需要array类型
            array(
                'data' => 'item1',
                'class' => array('list-item'),
                 ),
            array(
                'data' => 'item2',
                'class' => array('list-item'),
                 ),
            array(
                'data' => 'item3',
                'class' => array('list-item'),
                 ),
            array(
                'data' => 'item4',
                'class' => array('list-item'),
                 ),
        ),
        // 列表的标题
        'title' => array('list title'),
        // 主要的值为'ul'、'ol'
        'type' => 'ul',
        // ul的属性
        'attributes' => array(
            'class' => array(
                'ul-calss1',
                'ul-class2',
            ),
        'id' => 'ul-id-name',
        ),
    )
);

$html_item_list_renderable_array = array(
    '#theme' => 'item_list'
    '#items' => array(
            //如果item是字符串,这字符串就是每一个li的内容
            //如果item是array,这array的data键的值,是li的内容,其他的键则作为li标签的属性
            array(
                'data' => 'item1',
                'class' => 'list-item',
                 ),
            array(
                'data' => 'item2',
                'class' => 'list-item',
                 ),
            array(
                'data' => 'item3',
                'class' => 'list-item',
                 ),
            array(
                'data' => 'item4',
                'class' => 'list-item',
                 ),
        ),
        // 列表的标题
    '#title' => array('list title'),
    // 主要的值为'ul'、'ol'
    '#type' => 'ul',
    // ul的属性
    '#attributes' => array(
        'class' => array(
            'ul-calss1',
            'ul-class2',
        ),
        'id' => 'ul-id-name',
        ),
    ),
);


在所有theme函数需要的参数数组中的每一个键前添加’#’,并添加’#theme’键,在’#theme’键中指定相关的渲染函数,就完成了可渲染数组的构造。注意’#theme’键指定的渲染函数必须先由系统定义,或是由hook_theme()函数注册才可以。

自定义element渲染函数的主要流程为:

  • 利用hook_theme()注册element
  • 利用theme_element()创建theme_element的回调响应

注意这里的element主要是指html组件,如字段、表单元素、自定义列表等

例如,我们创建一个自定义的html组件-表单checkboxes组件的自定义渲染函数


/** * Implements hook_theme(). * 注册theme_custom_form_element_checkboxes()渲染函数到drupal的主题层中 */ function module_name_theme($existing, $type, $theme, $path) { return array( 'custom_form_element_checkboxes' => array('render element' => 'element'), ); } function theme_custom_form_element_checkboxes($variables){ // do something here // 将处理后的结果$output作为响应返回 return $output; }

安装python mysql中间件

自从Oracle收购mysql之后,原引领mysql的创始人就离开了mysql,创立了新的mariadb,表示他不再看好mysql。

而今天让我遇到了一个问题,正好证明了这一点。最近在做的一个小玩意中,有很多经过python收集和处理过的数据,需要存入数据库,而经常使用mysql,把mysql作为数据库的载体自然是在正常不过了。

最新的python版本是3,而python官方团队也在有条不紊的由python2过渡到python3,因此整个项目都是基于python3的。python操作mysql数据库需要安装mysql-python的驱动。而官方提供了很多可选项,而通过对比之后,mysqlclient是完全支持python3的,并且基于C,速度很快,因此最佳选择方案就是它了。

按照官方文档安装 mysqlclient,PS,我的环境是Mac

# 安装mysql驱动,你的电脑很可能已经安装过了
brew install mysql-connector-c

# 安装mysqlclient
pip install mysqlclient

然后万恶的报错了,你懂的。我用了这么多年的mysql,今天是第一次遇到了这么雷人的错误。以往遇到mysql的问题,随手google,立马问题就能解决,但是今天不一样了。逛了2个小时的Stack Overflow,仍然没有结果。下面是报的错误。

Collecting mysqlclient
  Using cached mysqlclient-1.3.10.tar.gz
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/private/var/folders/rv/a_a/T/pip-build-nyaa8t95/mysqlclient/setup.py", line 17, in <module>
        metadata, options = get_config()
      File "/private/var/folders/rv/a_a/T/pip-build-nyaa8t95/mysqlclient/setup_posix.py", line 54, in get_config
        libraries = [dequote(i[2:]) for i in libs if i.startswith('-l')]
      File "/private/var/folders/rv/a_a/T/pip-build-nyaa8t95/mysqlclient/setup_posix.py", line 54, in <listcomp>
        libraries = [dequote(i[2:]) for i in libs if i.startswith('-l')]
      File "/private/var/folders/rv/a_a/T/pip-build-nyaa8t95/mysqlclient/setup_posix.py", line 12, in dequote
        if s[0] in "\"'" and s[0] == s[-1]:
    IndexError: string index out of range

左右检查python-install和pip的版本,都是最新的稳定版,没有任何问题。然后就在github遇到另一个哥们,遇到和我一模一样的问题,我们都是MAMP+Python3.6+mysql5.6。连报的错误都一样,唯一不同的是,他是virtualenv,
而我是pyenv,实际上这两者并没有区别。

这个问题,mysqlclient开发小组的核心成员,来自日本的大神INADA Naoki给出了临时性的解决办法。他说,这是由于mysql的bug引起的问题,自十一月份开始陆续有人遇到这个问题,这个问题也让他很烦躁,是啊,这是Oracle的问题,而大家却都来找mysqlclient提交bug,是你你能不气,这锅至少我也不愿意背。具体可以去翻github mysqlclient的issue

出现这个问题的原因是mysql-connector-c中配置项有误。

具体针对mac来说,你需要顺藤摸瓜找到mysql_config的真身,即

/usr/local/Cellar/mysql-connector-c/6.1.11/bin/mysql_config

cd /usr/local/Cellar/mysql-connector-c/6.1.11/bin/mysql_config

# 修改前先备份
cp  mysql_config mysql_config.backup

# 使用vi修改配置文件
sudo vi mysql_config

# 114 gg跳转到 114行

将
> # Create options 
> libs="-L$pkglibdir"
> libs="$libs -l "

替换为

> # Create options 
> libs="-L$pkglibdir"
> libs="$libs -lmysqlclient -lssl -lcrypto"



然后保存即可。

#  然后重新运行mysqlclient安装命令,之后一切顺利,大功告成
pip install mysqlclient

注意,这是一种dirty fix,不优雅,我们不应该去修改源码,但是这是目前一种可行的办法,祈祷Oracle尽快修复这个问题吧。

使用python MySQLdb (由python/mysqlclient提供)


# 打开python交互模式 import MySQLdb # 如果以上命令没有弹出任何错误则一切正常,如果报错,这说明你的安装有误。 # 连接数据库 db = MySQLdb.connect("localhost","db_user","passwd","db_name") # 如果你是MAC,你很可能会报以下错误 > Traceback (most recent call last): > File "<stdin>", line 1, in <module> > File "/Users/username/.pyenv/versions/3.5.2/lib/python3.5/site-packages/MySQLdb/__init__.py", line 86, in Connect return Connection(*args, **kwargs) > File "/Users/username/.pyenv/versions/3.5.2/lib/python3.5/site-packages/MySQLdb/connections.py", line 204, in __init__ super(Connection, self).__init__(*args, **kwargs2) > _mysql_exceptions.OperationalError: (2002, "Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)")
# 在bash下查看mysql_config的socks配置选项
mysql_config --socket

> /tmp/mysql.sock 

这个错误看起来很熟悉,是因为MAMP环境下,mysql socks的位置在MAMP的安装目录下,并不在/tmp/mysql目录下,MYSQLdb对象无法使用connect方法连接到mysql的socks自然就报错了。

两个解决办法:

  1. 从/Applications/MAMP/tmp/mysql/mysql.sock新建一个符号链接到/tmp/目录下,这种方式不推荐,因为MAC会自动更新系统自带的mysql版本,而下一次更新的时候,mysql socks又会被替换,到时候还得改。

  2. 直接将mysql_config的socks地址配置选项改为/Applications/MAMP/tmp/mysql/mysql.sock

sudo vi /usr/local/Cellar/mysql-connector-c/6.1.11/bin/mysql_config

# 找到103行  socket='/tmp/mysql.sock'
# 改为 socket='/Applications/MAMP/tmp/mysql/mysql.sock'

改完重新启动mysql和python终端


# 打开python交互模式进行测试 import MySQLdb db = MySQLdb.connect("localhost","db_user","passwd","db_name") # 使用cursor()方法获取操作游标 cursor = db.cursor() # 使用execute方法执行SQL语句 cursor.execute("SELECT VERSION()") # 使用 fetchone() 方法获取一条数据 data = cursor.fetchone() > 1 print('Database version : %s ' % data) > Database version : 5.6.35 # 关闭数据库连接 db.close()

到此,MAMP + Python3 整个流程走通,剩下的就是参照API写SQL了。 :)

服务器封IP段

垃圾留言 – SQL注入

最近公司的网站一直收到来自俄罗斯圣彼得堡的垃圾留言,该留言是在公司的询盘表单页面通过写入代码而试图以SQL注入的方式攻击我们的服务器。

实际上询盘页面的表单在存储到数据库之前是经过验证和纯文本化处理的,所以SQL注入是不会奏效的,让我们忍无可忍的是这种骚扰方式。

因为询盘页面的留言,会以邮件的形式发送到公司领导的邮箱里,进而由领导来统筹安排业务工作。也就是说每一条垃圾留言都会发送到领导邮箱,这种类型的垃圾留言不仅会占用正常的工作时间,很多时候也会影响一个人的情绪。因此对此类不怀好意的访客,就必须作针对性的处理。

Apache 封IP

Apache本身可以通过相关的设置项来封锁特定IP的访问,需要添加deny from ip-address规则,在使用了一段时间以后,发现这种封锁方式缺乏灵活性,因为很多时候攻击者都不是固定IP,而是ADSL动态拨号,每播一次号,他就会的获得一个新的IP地址。而且这种封锁方式只针对apache的web服务进行封锁,如果一个不坏好意的黑客盯上了你,而只封锁web服务,显然是不够的。

由于不可知的原因,我在使用apache配置项封锁垃圾用户时,有些时候这些规则不起作用,不知道是什么原因,由于没有特别多的时间来研究这一块,所以就改用iptables,即系统自带的防火墙来封锁这类用户。

Iptables

iptables是linux自带的防火墙,可以通过定义规则来对来自于某些ip地址的访问来做到封锁和放行,iptables是运行在TCP/IP层的应用,所以针对这类操作效率特别高,而且可以针对端口来定制化封锁,也就是说,不仅可以用来封锁ip对apache服务的访问,也可以用来封锁对sshd,ftp等,这也就是防火墙的强大之处。

连续ip地址到ip段CIDR

这个黑客IP地址来自于俄罗斯圣彼得堡,运营商为Petersburg Internet Network ltd,本着最小化误伤原则,所以我选择对该运行商的整个IP段进行封锁。话说,他应该是比较菜的,一个有经验的黑客至少会选择使用proxy代理来攻击,他要是用了代理,就要增加我的工作量了。

网上搜索了该电信运行商的ip段,大概有十几万个。

通过划分,基本分布在以下IP段。

5.8.0.0/18
5.8.64.0/20
5.8.80.0/21
5.8.88.0/22
5.8.92.0/23
5.8.94.0/24
5.8.95.0/25
5.8.95.128/26
5.8.95.192/27
5.8.95.224/28
5.8.95.240/29
5.8.95.248/30
5.8.95.252/31
5.8.95.254/32
5.101.0.0/16
5.188.0.0/16
5.189.192.0/18
31.44.176.0/20
31.184.192.0/18
37.139.32.0/19
46.161.0.0/18
146.185.192.0/18
188.143.128.0/17

由多个连续的IP地址计算IP段的表示方法,比较复杂,我看了两遍都没有完全理解,所以就找了一个连续ip地址到IP段CIDR的转换工具,可以比较方便的快速转换IP段。

防火墙规则

  1. 使用iptables-save保存目前的配置文件到/etc配置目录,方便以后的管理工作
    iptables-save > /etc/iptables.conf
    
  2. 编辑配置文件添加规则
    # Generated by iptables-save v1.4.21 on Fri Sep 15 11:59:48 2017
    *security
    :INPUT ACCEPT [20133319:2135655181]
    :FORWARD ACCEPT [0:0]
    :OUTPUT ACCEPT [19230211:40949012733]
    COMMIT
    # Completed on Fri Sep 15 11:59:48 2017
    # Generated by iptables-save v1.4.21 on Fri Sep 15 11:59:48 2017
    *raw
    :PREROUTING ACCEPT [20169476:2138079977]
    :OUTPUT ACCEPT [19230211:40949012733]
    COMMIT
    # Completed on Fri Sep 15 11:59:48 2017
    # Generated by iptables-save v1.4.21 on Fri Sep 15 11:59:48 2017
    *nat
    :PREROUTING ACCEPT [1133835:64620787]
    :INPUT ACCEPT [1103001:62861375]
    :OUTPUT ACCEPT [16742:1994771]
    :POSTROUTING ACCEPT [16742:1994771]
    COMMIT
    # Completed on Fri Sep 15 11:59:48 2017
    # Generated by iptables-save v1.4.21 on Fri Sep 15 11:59:48 2017
    *mangle
    :PREROUTING ACCEPT [20169476:2138079977]
    :INPUT ACCEPT [20169474:2138079897]
    :FORWARD ACCEPT [0:0]
    :OUTPUT ACCEPT [19230211:40949012733]
    :POSTROUTING ACCEPT [19230211:40949012733]
    COMMIT
    # Completed on Fri Sep 15 11:59:48 2017
    # Generated by iptables-save v1.4.21 on Fri Sep 15 11:59:48 2017
    *filter
    :INPUT ACCEPT [27869:2482139]
    :FORWARD ACCEPT [0:0]
    :OUTPUT ACCEPT [26342:47499744]
    :f2b-sshd - [0:0]
    :f2b-sshd-ddos - [0:0]
    -A INPUT -p tcp -m multiport --dports 22 -j f2b-sshd-ddos
    -A INPUT -p tcp -m multiport --dports 22 -j f2b-sshd
    -A f2b-sshd -s 5.101.40.10/32 -j REJECT --reject-with icmp-port-unreachable
    -A f2b-sshd -j RETURN
    -A f2b-sshd-ddos -j RETURN
    
    # Block all ip address from Russia St. Petersburg
    
    -A INPUT -s 5.8.0.0/18 -j DROP
    -A INPUT -s 5.8.64.0/20 -j DROP
    -A INPUT -s 5.8.80.0/21 -j DROP
    -A INPUT -s 5.8.88.0/22 -j DROP
    -A INPUT -s 5.8.92.0/23 -j DROP
    -A INPUT -s 5.8.94.0/24 -j DROP
    -A INPUT -s 5.8.95.0/25 -j DROP
    -A INPUT -s 5.8.95.128/26 -j DROP
    -A INPUT -s 5.8.95.192/27 -j DROP
    -A INPUT -s 5.8.95.224/28 -j DROP
    -A INPUT -s 5.8.95.240/29 -j DROP
    -A INPUT -s 5.8.95.248/30 -j DROP
    -A INPUT -s 5.8.95.252/31 -j DROP
    -A INPUT -s 5.8.95.254/32 -j DROP
    -A INPUT -s 5.101.0.0/16 -j DROP
    -A INPUT -s 5.188.0.0/16 -j DROP
    -A INPUT -s 5.189.192.0/18 -j DROP
    -A INPUT -s 31.44.176.0/20 -j DROP
    -A INPUT -s 31.184.192.0/18 -j DROP
    -A INPUT -s 37.139.32.0/19 -j DROP
    -A INPUT -s 46.161.0.0/18 -j DROP
    -A INPUT -s 146.185.192.0/18 -j DROP
    -A INPUT -s 188.143.128.0/17 -j DROP
    
    
    COMMIT
    
    
  3. 导入配置使iptables启用规则
    iptables-restore < /etc/iptables.conf
    
  4. 配置网络启动项,使网络运行时就加载该设置

    注意这里和系统启动项还是有区别的,debian系统启动项管理一般是借助init.d和update-rc.d,这里我们主要使用的是/etc/network/if-pre-up.d的网络预加载(姑且这么翻译吧)。

    cd /etc/network/if-pre-up.d
    touch iptables.sh
    

    编辑iptables.sh

    vi iptables.sh
    

    加入以下行

    #!/usr/bin/env bash
    /sbin/iptables-restore < /etc/iptables.conf
    

    添加执行权限 (仅root具有执行权限)

    chmod 700 iptables.sh  
    

以下为2018年02月23日更新。

每次添加完新的规则之后,需要重新应用iptables-restore

另外还需要iptables -L -n
查看规则是否被成功应用

到此,长久以来的被骚扰问题应该算是解决了。具体效果怎样,需要进一步的观察。

D7 theme API 概述

对于Drupal的主体来说,如果是比较简单的项目,直接在主题模板tpl的基础上覆写或是配合主题的预处理器基本上就可以实现需求。而对于高度定制化的需求,Drupal也可以很方便的实现需求,这里就不得不说到drupal在主题层面上提供了强大的API供大家使用。

主题渲染的流程

$flow
st=>start: 路由匹配回调执行 hook_menu callback
e=>end: 前端 html
op1=>operation: 可渲染数组 Renderable array
op2=>operation: 预处理加工 preprocessor
op3=>operation: 处理加工 processor
op4=>operation: 模板打印输出 tpl.php
st->op1->op2->op3->op4->e
$

页面html的生成与可渲染数组Renderable Array

对于D7来说,一个页面的整体完全是由一个大数组构建并按需将数组元素转化为html,构成页面的建筑材料以数组作为规范,也可以是一个AJAX响应,或者一个由hook_menu定义的路由回调的反馈结果,这也就意味着你的页面回调应该返回一个数组而不是字符串作为结果,数组的好处是结果可以很方便的增删改,而整串html拼接的字符操作起来灵活性很差。在这里我们统一将构成页面的数组称为可渲染数组(Renderable array).

一个D7页面响应回调的例子

//页面的两部分表格+分页,而实际的项目中,很可能还会有其他的元素,比如页头、页脚、侧边栏等加入构建完整页面所需要的数组中
//这个例子中,module_name_page函数很可能是一个menu路径的响应回调函数
//回调的结果$build就是一个可渲染数组

function module_name_page() {
  $build = array();
  $build['table'] = array(
    //键名含有'#'的表示的是主题的属性值,不带'#'的一般为该主题元素的子元素
    //这里需要注意的是#theme和#theme_wrappers这两个属性,这两个属性建对应的字符串值实际上是生成html的回调函数
    //'#theme'的值'table'负责表格的生成
    //'#theme_wrappers'在'#theme'执行完之后执行,可以在渲染后的html外添加自定义的html,例如在字段的html外添加字段组html的包裹元素如div等,注意'#theme_wrappers'的回调函数的执行结果必须包含元素的'#children'属性,该属性包含'#theme'回调函数生成的html和子元素
    '#theme' => 'table',
    '#header' => $header,
    '#rows' => $rows,
  );
  $build['pager'] = array(
    '#theme' => 'pager',
  );
  return $build;
}

注:关于 ‘#theme’和 ‘#theme_wrappers’,官网API文档drupal_render( )有详细说明

前端对响应回调的处理

针对响应的可渲染数组,Drupal将这些结果交给主题的预处理器(preprocessor)和处理器(processor)进一步加工处理以方便在PHPTemplate的模板文件tpl中使用。

以一个node的页面为例

node页面的内容(renderable array)由函数node_view提供,回调结果提供给template_preprocess_node( )来预处理,预处理的作用是将大数组Renderable Array分解为一个个小数组,而这些小数组可以直接在模板文件node.tpl.php直接打印出来,打印完的结果,就是最终的html页面了。


function node_view($node, $view_mode = 'full', $langcode = NULL) { if (!isset($langcode)) { $langcode = $GLOBALS['language_content']->language; } // Populate $node->content with a render() array. node_build_content($node, $view_mode, $langcode); $build = $node->content; // We don't need duplicate rendering info in node->content. unset($node->content); $build += array( '#theme' => 'node', '#node' => $node, '#view_mode' => $view_mode, '#language' => $langcode, ); // Add contextual links for this node, except when the node is already being // displayed on its own page. Modules may alter this behavior (for example, // to restrict contextual links to certain view modes) by implementing // hook_node_view_alter(). if (!empty($node->nid) && !($view_mode == 'full' && node_is_page($node))) { $build['#contextual_links']['node'] = array('node', array($node->nid)); } // Allow modules to modify the structured node. $type = 'node'; drupal_alter(array('node_view', 'entity_view'), $build, $type); return $build; } // 这里的view_mode、teaser、title、page等将可以在模板文件中被直接打印 function template_preprocess_node(&$variables) { $variables['view_mode'] = $variables['elements']['#view_mode']; // Provide a distinct $teaser boolean. $variables['teaser'] = $variables['view_mode'] == 'teaser'; $variables['node'] = $variables['elements']['#node']; $node = $variables['node']; $variables['date'] = format_date($node->created); $uri = entity_uri('node', $node); $variables['node_url'] = url($uri['path'], $uri['options']); $variables['title'] = check_plain($node->title); $variables['page'] = $variables['view_mode'] == 'full' && node_is_page($node); // Flatten the node object's member fields. $variables = array_merge((array) $node, $variables); // Helpful $content variable for templates. $variables += array('content' => array()); foreach (element_children($variables['elements']) as $key) { $variables['content'][$key] = $variables['elements'][$key]; } ... }

node.tpl.php模板文件

<div id="node-<?php print $node->nid; ?>" class="<?php print $classes; ?> clearfix"<?php print $attributes; ?>>

  <?php print $user_picture; ?>

  <?php print render($title_prefix); ?>
  <?php if (!$page): ?>
    <h2<?php print $title_attributes; ?>><a href="<?php print $node_url; ?>"><?php print $title; ?></a></h2>
  <?php endif; ?>
  <?php print render($title_suffix); ?>

  <?php if ($display_submitted): ?>
    <div class="submitted">
      <?php print $submitted; ?>
    </div>
  <?php endif; ?>

  <div class="content"<?php print $content_attributes; ?>>
    <?php
      // We hide the comments and links now so that we can render them later.
      hide($content['comments']);
      hide($content['links']);
      print render($content);
    ?>
  </div>

  <?php print render($content['links']); ?>

  <?php print render($content['comments']); ?>

</div>

为什么要折腾 Linkedin

回答这个问题,需要先回答什么是Linkedin,Linkedin是面向职场人员的社交平台,也就是说这是一个和工作相关的社交平台,与Facebook晒吃喝晒美照晒萌娃不同的是,这里晒的是职场精英的工作经历和工作能力,晒他们帮助老板挣钱的能力,所以对于B端(B2B)来说,在这里更有可能找到理想的客户和合作伙伴。

如何去拓展linkedin社交圈子

  1. 创建内容
    • 个人职场照片
    • 完善的个人profile
    • 完善的工作经历与公司介绍
    • 公司的Group页面
    • 动态更新公司最近的events和stories

    这些工作的目的,只有一个,告诉你的潜在客户,你是真实的,你的公司是真实的,这样才有信任的一个基础。

  2. 拓展圈子(Connect)

行业大佬
优秀同行

linkedin仅仅针对connections来说,是没有办法通过搜寻相关的关键词来定位你要添加的人的,因此最好的办法莫过于借助以上两种途径。其中如果你添加了同行,也就意味着,在你圈子拓展的过程中,你的资源间接的也可以被你的同行所利用。这里如何拿下客户,就得靠公司实力和自己的真本事了。

  1. 使用工具

我不建议大家每天花费太多的时间在社交媒体上找客户,社交媒体平均的转化率是低于一般的途径的,而原因主要为三点,流量的垄断,人们的行为习惯以及行业的特点。

针对B2B来说,长久以来人们习惯通过搜索引擎,B2B平台来找供应商,这两种方式占据了流量的绝大部分,但是这也并不意味着社交媒体这一块就挖不到客户,或是社交媒体无用论。社媒的运作,需要一个持久的过程,其作用也是厚积薄发的。社交媒体上公司的动态更新与同步,配合视频图文,可以提高公司的真实性和可信度,从侧面的角度提高公司营销在传统途径的转化率,降低平均的询盘成本。

只有量变才能积累质变,如果你的linkedin账户只有300个connection,而你却想从这300个connection里得到一个订单,这其实是不现实的。 而从30000个connection里得到一个订单,其实是很容易的。

工欲善其事必先利其器,Linkedin的操作需要工具的辅助来提高效率。如果每天要我花费半小时的时间来刷connect页面,然后一个一个的去点击按钮connect,在这期间我什么干不了,只能机械的重复动作,一天两天是可以的,时间长了人是会发疯的,就如同以前要求每个业务员每天发布30个产品一样。

# 2017年08月01日09:39:36
# 由于前一段时间Linkedin改版,以上代码已经失效
#
jQuery('.button-secondary-small').each(function(index, value) {
  setTimeout(function() {
    jQuery(value).trigger('click');
  }, index * 1000);
});

Lindedin页面采用的是AJAX载入实现页面无需重载而更新,以上的代码是基于jquery插件Linkedin自动connect代码,美中不足就是无法自动下拉页面,只有手动的去滑动页面才能一次添加更多的人,离实现我们的需求还差的很远。因此,我不得不利用零零碎碎的时间,自己来开发一个小工具来帮助工作提高效率。说实话,虽然我没有学过javascript,但是这门语言似乎没有我想想的那么难。或许代码写的比较烂,我需要的功能基本都实现了。

/*
 * Linkedin 1 click connect tools
 *
 * Version: 1.0-alpha
 * This tools is developed based on Chrome for MAC 60.0
 * Previous versions support will not be considered
 * Use at your own risk
 * /


/* Find the embed element */
mainDiv = document.querySelector(".mn-connections-summary__no-top-border-radius.p0");


/* Add the action form to DOM */

form = document.createElement("form");
form.className = "mn-connections-summary ember-view";
mainDiv.appendChild(form);
form.style.width = "90%";

/*
 * Button START SCROLL will only scroll the page, and button STOP SCROLL will stop scroll.
 * After the scroll process is done, then you can hit the CONNECT All button to connect people on this page.
 * ONE CLICK TO CONNECT will auto scroll down and connect people with your settings or default if none available.
 */

 form.innerHTML = ''
    + '<lable style="font-size: 10px; text-align: left;">How much time to spare for connecting people? - 10 mins default</lable>'
    + '<input style="margin-top:5px; margin-right: 5px;"class= "time-mins-scroll" placeholder="number of minutes" type="text" name="scroll_time">'
    + '<lable style="font-size: 10px; text-align: left;">Page fresh interval - 5s default</lable>'
    + '<input style="margin-top:5px; margin-right: 5px;"class= "scroll-interval" placeholder="number of seconds" type="text" name="scroll_interval">'
    + '<input style="margin-top:5px; margin-right: 5px;"class= "start-scroll-btn button-secondary-medium" type="button" value="Start scroll">'
    + '<input style="margin-top:5px; margin-right: 5px;"class= "stop-scroll-btn button-secondary-medium" type="button" value="Stop scroll">'
    + '<input style="margin-top:5px; margin-right: 5px;"class= "connect-all-btn button-secondary-medium" type="button" value="Connect all">'
    + '<input style="margin-top:5px; margin-right: 5px;"class= "one-click-connect-btn button-secondary-medium" type="button" value="One Click to Connect all">';

var startScrollBtn = document.querySelector(".start-scroll-btn"),
stopScrollBtn = document.querySelector(".stop-scroll-btn"),
connectAllBtn = document.querySelector(".connect-all-btn"),
oneClickConnectBtn = document.querySelector(".one-click-connect-btn");

var cycleTimes = 0, scrollCycle = 10 * 60, scrollInterval = 5, scrollIntevalInit, peoplesOnPage = {};

(document.querySelector(".time-mins-scroll").value == "") ? scrollCycle = 10 * 60 : scrollCycle = document.querySelector(".time-mins-scroll").value * 60;

(document.querySelector(".scroll-interval").value == "") ? scrollInterval = 5 : scrollInterval = document.querySelector(".scroll-interval").value;


scrollToBottom = function(){
    scrollHeight = document.documentElement.scrollHeight;
    window.scrollTo(0, scrollHeight);
    cycleTimes = cycleTimes + 1;
    console.log("page refresh #" + cycleTimes);
};

//@todo 阻止重复点击事件


startScrollBtn.addEventListener("click", function(){

    (document.querySelector(".time-mins-scroll").value == "") ? scrollCycle = 10 * 60 : scrollCycle = document.querySelector(".time-mins-scroll").value * 60;
    (document.querySelector(".scroll-interval").value == "") ? scrollInterval = 5 : scrollInterval = document.querySelector(".scroll-interval").value;
    console.log(scrollCycle);
    console.log(scrollInterval);
    if(cycleTimes <= (scrollCycle / scrollInterval)){
        scrollIntevalInit = setInterval(scrollToBottom, scrollInterval * 1000);
    }
}, false);


//@todo 阻止重复点击事件

stopScrollBtn.addEventListener("click",function(){
    clearInterval(scrollIntevalInit);
});

connectAllBtn.addEventListener("click",function(){

    clearInterval(scrollIntevalInit);

    peoplesOnPage = document.querySelectorAll(".mn-pymk-list__action-container .button-secondary-small");
    for (var i = 0 ; i < peoplesOnPage.length; i++) {
        (function(index){
            setTimeout(
                function(){
                    peoplesOnPage[index].click();
                    console.log("#" + index + "clicked");
                }, scrollInterval * index * 1000);
        })(i);
    }
});

oneClickConnectBtn.addEventListener("click", function () {

    (document.querySelector(".time-mins-scroll").value == "") ? scrollCycle = 10 * 60 : scrollCycle = document.querySelector(".time-mins-scroll").value * 60;
    (document.querySelector(".scroll-interval").value == "") ? scrollInterval = 5 : scrollInterval = document.querySelector(".scroll-interval").value;

    scrollIntevalInit = setInterval(scrollToBottom, scrollInterval * 1000);

    if(cycleTimes > (scrollCycle/scrollInterval)){

        clearInterval(scrollIntevalInit);

        peoplesOnPage = document.querySelectorAll(".mn-pymk-list__action-container .button-secondary-small");

        for (var i = 0 ; i < peoplesOnPage.length; i++) {
            (function(index){
                setTimeout(
                    function(){
                        console.log(index);
                        peoplesOnPage[index].click();
                    }, scrollInterval * index * 1000);
            })(i);
        }
    }
});

Drupal Entity API

D8 Entity可以通过Entity Validation API对多种方式(Form、REST)保存的Entity进行验证。

Entity是含有预定义方法的类

方法类型 方法示例
一般方法 $entity->id()
特定entity的特定方法 $node->getTitle()

注: 通常这些方法定义在interfaces中,目前还没有比较完备的D8 Traits文档
Classes, traits, and interfaces

处理器/调用 Handlers

entity处理器实际上是预定义了对entity进行处理的方法的类。

存储处理器 Storage handler – 支持载入、保存和删除entity等操作,包括对entity多次修订版本、多语言翻译版本和配置字段。

另外,除了存储处理器,还有其他的处理器例如许可控制处理器AccessControlHandler、视图处理器Viewing,列表处理器Listings,表单处理器Forms。

Entity的两种类型

  • Configuration Entity (由D8的Configuration系统使用,可以在安装时配置默认选项,注意Configuration Entity以文件的形式存在,而非存储在数据库表中,例如D8很多的模块下都有config目录,而该目录下的yml文件,一般都是Configuration Entity的配置文件。)
  • Content Entity (包含可配置的基础字段,并且可以根据需要添加其他额外字段,支持多版本和多语言。)

Entity类型

规范

模块的Entity的命名规范
例如:Transport模块定义了名称为Car的entity


/* Transport Car * Defines the Car entity class * * @ContentEntityType( * id = "transport_car", * label = @Translation("Transport Car"), * bundle_label = @Translation("Transport"), * handlers = { * "storage" = "Drupal\transport\CarStorage", * "storage_schema" = "Drupal\transport\CarStorageSchema", * "view_builder" = "Drupal\transport\CarViewBuilder", * "access" = "Drupal\transport\CarAccessControlHandler", * "views_data" = "Drupal\transport\CarViewsData", * "form" = { * "default" = "Drupal\transport\CarForm", * "delete" = "Drupal\transport\Form\CarDeleteForm", * }, * "route_provider" = { * "html" = "Drupal\transport\Entity\CarRouteProvider", * }, * "translation" = "Drupal\transport_car\CarTranslationHandler" * }, * base_table = "transport_car_data", * data_table = "transport_car_field_data", * translatable = TRUE, * list_cache_contexts = { "user.transport_car_grants:view" }, * entity_keys = { * "id" = "cid", * "bundle" = "transport type", * "label" = "title", * "langcode" = "langcode", * "uuid" = "uuid", * }, * field_ui_base_route = "entity.transport_car.edit_form", * common_reference_target = TRUE, * permission_granularity = "bundle", * links = { * "canonical" = "/transport/car/{transport_car}", * "delete-form" = "/transport/car/{transport_car}/delete", * "edit-form" = "/transport/car/{transport_car}/edit", * } * ) * * * transport/src/Entity/Car.php * Car Entity类定义文件 * */ namespace Drupal\trasnsport\Entity class Car extend ContentEntityBase { use EntityChangedTrait; public function carFunction1 () { //do something }; } # 位于modules/custom/transport/src/Entity/目录下

使用Gitlab

# Git global setup

git config --global user.name "your-user-name"
git config --global user.email "your-user-name@email.com"

# Create a new repository

git clone git@gitlab.com:your-user-name/repo-name.git
cd test
touch README.md
git add README.md
git commit -m "add README"
git push -u origin master

# Existing folder

cd existing_folder
git init
git remote add origin git@gitlab.com:your-user-name/repo-name.git
git add .
git commit -m "Initial commit"
git push -u origin master

# Existing Git repository

cd existing_repo
git remote add origin git@gitlab.com:your-user-name/repo-name.git
git push -u origin --all
git push -u origin --tags

为什么要安装Aria2

百度网盘限速,而Aria2是个十分强大的文件分片多线程下载工具,可以有效抵制限速。

为什么要采取编译安装

  1. Aria2的参数中-max-server-connection和-min-split-files很大程度上决定了网盘的下载速度。
  2. 默认参数中-max-server-connection(1,16),-min-split-files(1m,1g),下载服务器最大连接数16,肯定是不能满足高速下载要求的,而最小文件分片1M,尤其是当我们下载网盘上的小文件是,10M的文件按照最小1M的规格,要分成10片,Aria2最多只会分配10个线程去下载,即使是10M的小文件,也要耗用将近好几分钟的时间,效率党简直不能忍啊。
  3. 编译安装采取源代码的安装方式,允许我们修改Aria2核心的运行参数。在这里,我将-max-server-connection最高设置为256,-min-split-files最小设置为256k,大家可以根据自己电脑的性能来合理设置参数以提高下载速度。

具体安装过程

#下载源码
cd ~/Download/
git clone https://github.com/aria2/aria2.git
#修改源码
#aira2的基本配置选项大多都存储在文件OptionHandlerFactory.cc中
vi aria2/src/OptionHandlerFactory.cc
#定位到441行
#将服务器最大连接数16修改为256
    OptionHandler* op(new NumberOptionHandler(PREF_MAX_CONNECTION_PER_SERVER,
                                              TEXT_MAX_CONNECTION_PER_SERVER,
                                           // "1", 1, 16, 'x'));
                                              "1", 1, 256, 'x'));
#定位到第503行
#将最文件分片大小设置为256_k
#到此源代码需要修改的地方改完了
  OptionHandler* op(new UnitNumberOptionHandler(
//PREF_MIN_SPLIT_SIZE, TEXT_MIN_SPLIT_SIZE, "1M", 1_m, 1_g, 'k'));
 PREF_MIN_SPLIT_SIZE, TEXT_MIN_SPLIT_SIZE, "1M", 256_k, 1_g, 'k'));

#安装编译依赖
#以下依赖仅限于macosx,linux参照官方文档
brew install libtool
brew install automake
brew install autoconf-archive

#编译安装
autoconf configure.ac
autoconf -i
./configure
make check
make
sudo make install
#如果你遇到 error: possibly undefined macro这说明编译缺乏依赖,需要安装相关依赖
#
#查看编译后的Aria2参数
aria2c --help
# 可以看到设置项的参数范围已经改变了
 -x, --max-connection-per-server=NUM The maximum number of connections to one
                              server for each download.

                              Possible Values: 1-256
                              Default: 1
                              Tags: #basic, #http, #ftp

 -k, --min-split-size=SIZE    aria2 does not split less than 2*SIZE byte range.
                              For example, let's consider downloading 20MiB
                              file. If SIZE is 10M, aria2 can split file into 2
                              range [0-10MiB) and [10MiB-20MiB) and download it
                              using 2 sources(if --split >= 2, of course).
                              If SIZE is 15M, since 2*15M > 20MiB, aria2 does
                              not split file and download it using 1 source.
                              You can append K or M(1K = 1024, 1M = 1024K).

                              Possible Values: 262144-1073741824
                              Default: 1M
                              Tags: #basic, #http, #ftp

#修改~/.aria2/aria2.conf配置文件使Aria2新设置生效
vi ~/.aria2/aria2.conf
#这里我只截取主要设置项
#断点续传
continue=true
#同服务器连接数
max-connection-per-server=256
#最小文件分片大小, 下载线程数上限取决于能分出多少片, 对于小文件重要
min-split-size=256k
#单文件最大线程数, 路由建议值: 5
split=256
#下载速度限制
max-overall-download-limit=0
#单文件速度限制
max-download-limit=0
...

到此结束。

PS.本文中的 shell 代指 bash

Shell 中的括号

小括号()

# 应用实例
# ()作为一个命令组
if ($i<5)
# "[" 意为test, "-lt" 意为less than 小于, "]"  意为关闭条件判断
if [ $i -lt 5 ]
# "-ne" 意为 不等于,] 后面的 "-a" 为逻辑操作符,等价于 "&&"
if [ $a -ne 1 ] -a [ $a != 2 ]
if [ $a -ne 1] && [ $a != 2 ]
if [[ $a != 1 && $a != 2 ]]

for i in $(seq 0 4);do echo $i;done
for i in `seq 0 4`;do echo $i;done
for ((i=0;i<5;i++));do echo $i;done
for i in {0..4};do echo $i;done



# 循环读取文件中的每一行并将该行作为参数,执行相关的命令
while read p; do
  echo "$p"
done < file.txt

  1. 单小括号 ()
    • 命令组。括号中的命令将会新开一个子shell顺序执行,所以括号中的变量不能够被脚本余下的部分使用。括号中多个命令之间用分号隔开,最后一个命令可以没有分号,各命令和括号之间不必有空格。
    • 命令替换。等同于cmd,shell扫描一遍命令行,发现了$(cmd)结构,便将$(cmd)中的cmd执行一次,得到其标准输出,再将此输出放到原来命令。有些shell不支持,如tcsh。
    • 用于初始化数组。如:array=(a b c d)。
  2. 双小括号 (( ))
    • 整数扩展。这种扩展计算是整数型的计算,不支持浮点型。((exp))结构扩展并计算一个算术表达式的值,如果表达式的结果为0,那么返回的退出状态码为1,或者 是”假”,而一个非零值的表达式所返回的退出状态码将为0,或者是”true”。若是逻辑判断,表达式exp为真则为1,假则为0。
    • 只要括号中的运算符、表达式符合C语言运算规则,都可用在$((exp))中,甚至是三目运算符。作不同进位(如二进制、八进制、十六进制)运算时,输出结果全都自动转化成了十进制。如:echo $((16#5f)) 结果为95 (16进位转十进制)
    • 单纯用 (( )) 也可重定义变量值,比如 a=5; ((a++)) 可将 $a 重定义为6。
    • 双括号中的变量可以不使用$符号前缀。括号内支持多个表达式用逗号分开。

中括号[]

  1. 单中括号 []
    • bash 的内部命令,[和test是等同的。如果我们不用绝对路径指明,通常我们用的都是bash自带的命令。if/test结构中的左中括号是调用test的命令标识,右中括号是关闭条件判断的。这个命令把它的参数作为比较表达式或者作为文件测试,并且根据比较的结果来返回一个退出状态码。if/test结构中并不是必须右中括号,但是新版的Bash中要求必须这样。
    • Test和[]中可用的比较运算符只有==和!=,两者都是用于字符串比较的,不可用于整数比较,整数比较只能使用-eq,-gt这种形式。无论是字符串比较还是整数比较都不支持大于号小于号。如果实在想用,对于字符串比较可以使用转义形式,如果比较”ab”和”bc”:[ ab \< bc ],结果为真,也就是返回状态为0。[ ]中的逻辑与和逻辑或使用-a 和-o 表示。
    • 字符范围。用作正则表达式的一部分,描述一个匹配的字符范围。作为test用途的中括号内不能使用正则。
    • 在一个array 结构的上下文中,中括号用来引用数组中每个元素的编号或是索引。
  2. 双中括号[[ ]]
    • [[是 bash 程序语言的关键字。并不是一个命令,[[ ]] 结构比[ ]结构更加通用。在[[和]]之间所有的字符都不会发生文件名扩展或者单词分割,但是会发生参数扩展和命令替换。
    • 支持字符串的模式匹配,使用=~操作符时甚至支持shell的正则表达式。字符串比较时可以把右边的作为一个模式,而不仅仅是一个字符串,比如[[ hello == hell? ]],结果为真。[[ ]] 中匹配字符串或通配符,不需要引号。
    • 使用[[ … ]]条件判断结构,而不是[ … ],能够防止脚本中的许多逻辑错误。比如,&&、||、<和> 操作符能够正常存在于[[ ]]条件判断结构中,但是如果出现在[ ]结构中的话,会报错。
    • bash把双中括号中的表达式看作一个单独的元素,并返回一个退出状态码。

关于单中括号 [] 和双中括号 [[]] 的区别,可以参照这里

大括号{}

$ ls {ex1,ex2}.sh
ex1.sh  ex2.sh
$ ls {ex{1..3},ex4}.sh
ex1.sh  ex2.sh  ex3.sh  ex4.sh
$ ls {ex[1-3],ex4}.sh
ex1.sh  ex2.sh  ex3.sh  ex4.sh
  1. 常规用法
    • 大括号拓展。

      (通配(globbing))将对大括号中的文件名做扩展。在大括号中,不允许有空白,除非这个空白被引用或转义。第一种:对大括号中的以逗号分割的文件列表进行拓展。如 touch {a,b}.txt 结果为a.txt b.txt。第二种:对大括号中以点点(..)分割的顺序文件列表起拓展作用,如:touch {a..d}.txt 结果为a.txt b.txt c.txt d.txt

    • 代码块,又被称为内部组,这个结构事实上创建了一个匿名函数。

      与小括号中的命令不同,大括号内的命令不会新开一个子shell运行,即脚本余下部分仍可使用括号内变量。括号内的命令间用分号隔开,最后一个也必须有分号。{}的第一个命令和左括号之间必须要有一个空格。

  2. 几种特殊的替换结构

    • ${var:-string}和${var:=string}:若变量var为空,则用在命令行中用string来替换${var:-string},否则变量var不为空时,则用变量var的值来替换${var:-string};对于${var:=string}的替换规则和${var:-string}是一样的,所不同之处是${var:=string}若var为空时,用string替换${var:=string}的同时,把string赋给变量var: ${var:=string}很常用的一种用法是,判断某个变量是否赋值,没有的话则给它赋上一个默认值。
    • ${var:+string}的替换规则和上面的相反,即只有当var不是空的时候才替换成string,若var为空时则不替换或者说是替换成变量 var的值,即空值。(因为变量var此时为空,所以这两种说法是等价的)
    • ${var:?string}替换规则为:若变量var不为空,则用变量var的值来替换${var:?string};若变量var为空,则把string输出到标准错误中,并从脚本中退出。我们可利用此特性来检查是否设置了变量的值。
    • 补充扩展:在上面这五种替换结构中string不一定是常值的,可用另外一个变量的值或是一种命令的输出。
  3. 四种模式匹配替换结构
    • ${variable%pattern},这种模式时,shell在variable中查找,看它是否一给的模式pattern结尾,如果是,就从命令行把variable中的内容去掉右边最短的匹配模式
    • ${variable%%pattern},这种模式时,shell在variable中查找,看它是否一给的模式pattern结尾,如果是,就从命令行把variable中的内容去掉右边最长的匹配模式
    • ${variable#pattern} 这种模式时,shell在variable中查找,看它是否一给的模式pattern开始,如果是,就从命令行把variable中的内容去掉左边最短的匹配模式
    • ${variable##pattern} 这种模式时,shell在variable中查找,看它是否一给的模式pattern结尾,如果是,就从命令行把variable中的内容去掉右边最长的匹配模式
    • 这四种模式中都不会改变variable的值,其中,只有在pattern中使用了匹配符号时,%和%%,#和##才有区别。结构中的pattern支持通配符,表示零个或多个任意字符,?表示零个或一个任意字符,[…]表示匹配中括号里面的字符,[!…]表示不匹配中括号里面的字符
$ var=testcase
$ echo $var
testcase
$ echo ${var%s*e}
testca
$ echo $var
testcase
$ echo ${var%%s*e}
te
$ echo ${var#?e}
stcase
$ ${var##?e}
stcase
$ echo ${var##*e}

$ echo ${var##*s}
e
$ echo ${var##test}
case

Test 与 逻辑运算

判断一串命令的执行结果

# 判断文件夹知否为空
test `ls some-dir | wc -c` -eq 0 && echo "为空" || echo "非空"
# 以上命令等价于
test $(ls some-dir | wc -c) -eq 0 && echo "为空" || echo "非空"
[ $(ls some-dir | wc -c) -eq 0 ] && echo "为空" || echo "非空"
[[ $(ls some-dir | wc -c) -eq ]] && echo "为空" || echo "非空"
[[ $(ls some-dir | wc -c)=0 ]] && echo "为空" || echo "非空"
# 在实际的脚本中推荐使用含 双中括号 的第四种写法 "="等价判断两边不得含有空格,否则报错

为Composer/Git/Gulp/Brew/Npm/Pip/Drush添加自动补全

  1. 安装bash-completion

    Mac下是brew, Debian下面是Apt-get

  2. 从github的bash-it项目下载需要的自动补全bash脚本,放到/etc/bash-completion目录下

  3. vi /etc/bashrc 加入如下代码

function bash_completion_load(){

    local bash_completion=/etc/bash-completion

    if [[ -e $bash_completion ]] && [[ -d $bash_completion ]] && [[ $(ls $bash_completion | wc -c)!=0 ]]; then
        for completion_item in $(ls $bash_completion); do
            if [[ ${completion_item##*.}='bash' ]]; then
                chmod +x $bash_completion/$completion_item
                source $bash_completion/$completion_item
            fi
        done
    fi
}

bash_completion_load;

至于Debian Linux来说,直接把从github下载的补全脚本文件直接放到/etc/bash-completion.d/目录下就可以了。
需要添加执行权限

Drupal8 / Composer / Drush / Drupal Console 相关

修复features暴力卸载

修复由暴力卸载features模块生成的插件而导致的”The following module is missing from the file system…drupal…bootstrap…250…”错误。
drupal官网也有关于此问题的链接

  1. drush pmu module-name
  2. composer remove drupal/module-name
  3. drush sql-query “DELETE FROM key_value WHERE collection=’system.schema’ AND name=’module-name’;”
  4. drush cr
  5. 刷新 status report 页面,看错误是否还在。
drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='b2bcms_catalogue';"
drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='b2bcms_manufacturing_process';"
drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='b2bcms_products';"
drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='b2bcms_quick_details';"
drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='b2bcms_standards';"
drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='config_update';"
### 命令格式
drush sql-query "DELETE FROM key_value WHERE collection='system.schema' AND name='module_name';"

2017年07月25日20:43:25更新

实际上仅仅从上述数据库记录中依旧不能完全删除暴力卸载的残留。

Features包导入的配置项以自定义模块的方式安装到drupal中,除了在上述数据库中记录外,还会在core.extension配置项中留下记录。core.extension目测是以数据库记录的形式保存在数据库中,而不是yml明文配置文件的形式存储,对应网站后台的操作就是admin/modules, 这里记录用户安装卸载的模块的情况,所以需要从数据库中删除记录,而我找了一圈也没有找到具体这些模块记录是存储在哪一个表中,而系统的很多配置项大多是以序列化之后的形式存储在key_value表中,直接在数据库中修改序列化之后的数据是不现实的,而且这里也没有找到core.extension配置项。

除了直接动数据库这样的危险操作,还可以直接使用drush内置的config选项来灵活的修改系统选项。

#查看有哪些config相关的命令可以用
drush | grep config
 core-config (conf,    Edit drushrc, site alias, and Drupal settings.php files.
 config)
 core-quick-drupal     Download, install, serve and login to Drupal with minimal configuration and dependencies.
 site-install (si)     Install Drupal along with modules/themes/configuration using the specified install profile.
Config commands: (config)
 config-delete (cdel)  Delete a configuration object.
 config-edit (cedit)   Open a config file in a text editor. Edits are imported into active configuration after closing editor.
 config-export (cex)   Export configuration to a directory.
 config-get (cget)     Display a config value, or a whole configuration object.
 config-import (cim)   Import config from a config directory.
 config-list (cli)     List config names by prefix.
 config-pull (cpull)   Export and transfer config from one environment to another.
 config-set (cset)     Set config value directly. Does not perform a config import.

 #查看模块配置信息
 drush config-get core.extension

 #修改模块配置信息,删除无效模块
 drush config-edit core.extension
 #这里drush会呼叫vi编辑器,删除干扰的无效模块记录就可以了
 #保存修改后的记录,终端会提示core.extension配置项已更新
 #询问我们是否导入新的配置项,选择Y导入即可
            core.extension  update
Import the listed configuration changes? (y/n): y
#drush config-import可能需要config_update模块支持,如果提示你安装,安装该模块就可以了。

Drupal console generate:module命令不存在的问题

  1. MAC MAMP环境
  2. Drupal基于Github Composer-Drupal环境安装
  3. Composer基于Brew安装
  4. Drupal Console基于composer安装

使用Composer安装Drupal Console的办法

# 添加需要模块
composer require drupal/console:~1.0 --prefer-dist --optimize-autoloader
# 安装模块依赖组件
composer update drupal/console --with-dependencies

这里面有个问题,composer是基于项目的包管理工具,安装的包都是针对当前项目的,所以这里安装的drupalconsole(以下简称drupal)并没有默认安装在系统的/bin,/usr/bin/,/usr/share/bin等系统默认可以查找二进制命令的目录下,所以当你尝试运行drupal的时候,很可能会shell会报错,找不到drupal命令。

即使是使用 composer global require drupal/console:@stable 全局安装并且export PATH=”$HOME/.composer/vendor/bin:$PATH”添加路径支持,那么在drupal安装的根目录(对于composer-drupal安装方式来说,就是项目根目录下的web目录)下执行 drupal genernate:module 你还是会收到shell提示如下错误:

[Symfony\Component\Console\Exception\CommandNotFoundException]
Command “generate:module” is not defined.

原因是因为drupal[drupal-console]是PHP脚本文件,运行时会有很多依赖,而全局化安装,一些具体的特殊命令例如generate等,依赖没有安装到位,所以就造成了这样的问题。

解决办法

  1. 针对 composer 安装 drupalconsole的方式 (注意,这里不要使用composer全局安装drupalconsole,安装了也不能解决你的问题)。
    cat << EOF >> ~/.bash_profile
    # 利用composer项目文件结构的特点,添加alias,使用相对路径来执行 drupalconsole 命令
    alias drupal='../vendor/drupal/console/bin/drupal'
    EOF
    source ~/.bash_profile
    

    这里需要注意的是,drupalconsole命令需要在web目录内执行,否则由于路径的问题,这会提示你找不到命令。

  2. 如果你使用drupalconsole官网curl命令下载安装的方式,很可能就不会出现这种问题。这种办法我目前还没有验证过。