├── CHANGELOG ├── README.md ├── config ├── setup.py ├── src ├── __init__.py ├── startup_wpupdate └── wpcore.py └── wpupdate.py /CHANGELOG: -------------------------------------------------------------------------------- 1 | ~~~~~~~~~~~~~~ 2 | version 0.4 3 | ~~~~~~~~~~~~~~ 4 | 5 | * added checkout from github instead of copying files over 6 | 7 | ~~~~~~~~~~~~~~ 8 | version 0.3 9 | ~~~~~~~~~~~~~~ 10 | 11 | * added support to automatically pull the latest version of plugins from api.wordpress.org then from there update plugins accordingly - note its not possible to grab versions and check to update, we actually have to pull the zip and update each time. Little less efficient but it works. Will work on another solution later. 12 | * added better handling if a plugin wasn't found, it would let you know that its no longer in the wordpress repository 13 | 14 | ~~~~~~~~~~~~~~ 15 | version 0.2 16 | ~~~~~~~~~~~~~~ 17 | 18 | * added a config file with option to specify multiple wordpress installations 19 | * fixed a path typo for wp-content 20 | * major rewrite, no longer downloads the file each night and does hash comparisons, it pulls from wordpress' api then does a match under wp-includes/versions.php - much more efficient and don't need to maintain a working copy 21 | * rewrote it so that it only downloads latest.zip once if needed 22 | * added ability to specify multiple wordpress installations and as many as you want 23 | 24 | ~~~~~~~~~~~~~~ 25 | version 0.1 26 | ~~~~~~~~~~~~~~ 27 | 28 | * initial release of wpupdate 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WPUpdate is a simple update script that will automatically update your wordpress installation when a new release is out. It does this by doing a compare on Wordpresses update API on their website then doing a comparison under wp-includes/version.php. From there, it will determine if changes are needed and an update. After that it will automatically update wordpress, set the ownership to be restricted to only root:root for security reasons (except uploads). After that, it'll check again at 2AM. WPUpdate also updates third party plugins automatically as well. 2 | 3 | ### Features 4 | 5 | 1. Automatically check for updates once a day. This is through the /etc/init.d/wpupdate service. 6 | 7 | 2. If a new release is out, it'll automatically update Wordpress for you. 8 | 9 | 3. It will sleep until 2AM the next day, then check for an update again. 10 | 11 | 4. Allows for multiple wordpress installations, simply edit the config file located under /usr/share/wpupdate/config and add /var/www/blog1,/var/www/blog2 for multiple blogs. 12 | 13 | 5. Updates third party plugins automatically each time it goes through it's daily update. 14 | 15 | ### Bugs and enhancements 16 | 17 | For bug reports or enhancements, please open an issue here https://github.com/trustedsec/wpupdate/issues 18 | 19 | ### Project structure 20 | 21 | For those technical folks you can find all of the code in the following structure: 22 | 23 | - ```src/wpcore.py``` - main central code reuse for things shared between each module 24 | - ```setup.py``` - copies files to ```/usr/share/wpupdate/``` then edits ```/etc/init.d/wpupdate``` to ensure wpupdate starts per each reboot 25 | 26 | ### Supported platforms 27 | 28 | - Linux 29 | 30 | ### HOWTO Install 31 | 32 | Simply run python setup.py, hit yes to install. Note that if your installation isn't /var/www/ you will need to edit the source code. 33 | 34 | ### Future Plans 35 | 36 | - Completed future plans. 37 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | ###################################### 2 | # 3 | # configuration file for wpupdate 4 | # 5 | ###################################### 6 | # 7 | # 8 | ## This is the path for directories that wpupdate will automatically go and update. If you have multiple sites 9 | ## seperate them simply by commas. For example say I have two different blogs in /var/www/blog1 and /var/www/blog2 10 | ## if I add the line below to be /var/www/blog1,/var/www/blog2 it will iterate through the directories for you. 11 | BLOG_PATH="/var/www" 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # quick script for installing wpupdate 4 | # 5 | # 6 | import subprocess,re,os,shutil 7 | 8 | from src.wpcore import * 9 | 10 | print """ 11 | Welcome to the installer for WPUpdate. This will check once a day for updates within Wordpress. If 12 | a new version is detected, it will automatically download and install Wordpress for you automatically. 13 | 14 | Written by: Dave Kennedy (@HackingDave) 15 | """ 16 | 17 | def kill_wpupdate(): 18 | print "[*] Checking to see if WPUpdate is currently running..." 19 | proc = subprocess.Popen("ps -au | grep /usr/share/wpupdate/wpupdate.py", stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 20 | stdout = proc.communicate() 21 | try: 22 | for line in stdout: 23 | match = re.search("python /var/wpupdate/wpupdate.py", line) or re.search("python wpupdate.py", line) 24 | if match: 25 | print "[*] Killing running version of WPUpdate.." 26 | line = line.split(" ") 27 | pid = line[6] 28 | subprocess.Popen("kill %s" % (pid), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() 29 | print "[*] Killed the WPUpdate process: " + pid 30 | except: pass 31 | 32 | if os.path.isfile("/etc/init.d/wpupdate"): 33 | answer = raw_input("WPUpdate detected. Do you want to uninstall [y/n:] ") 34 | if answer.lower() == "yes" or answer.lower() == "y": 35 | answer = "uninstall" 36 | 37 | if not os.path.isfile("/etc/init.d/wpupdate"): 38 | answer = raw_input("Do you want to proceed to install wpupdate? [y/n]: ") 39 | 40 | if answer.lower() == "y" or answer.lower() == "yes" or answer.lower() == "": 41 | print "[*] Checking out WPUpdate through github to /usr/share/wpupdate" 42 | # if old files are there 43 | if os.path.isdir("/usr/share/wpupdate"): 44 | shutil.rmtree('/usr/share/wpupdate') 45 | subprocess.Popen("git clone https://github.com/trustedsec/wpupdate /usr/share/wpupdate", shell=True).wait() 46 | # os.makedirs("/usr/share/wpupdate") 47 | # subprocess.Popen("cp -rf * /usr/share/wpupdate/", shell=True).wait() 48 | 49 | print "[*] Installing the service for you.." 50 | 51 | 52 | if not os.path.isfile("/etc/init.d/wpupdate"): 53 | fileopen = file("src/startup_wpupdate", "r") 54 | config = fileopen.read() 55 | filewrite = file("/etc/init.d/wpupdate", "w") 56 | filewrite.write(config) 57 | filewrite.close() 58 | print "[*] Triggering update-rc.d on wpupdate to automatically start..." 59 | subprocess.Popen("chmod +x /etc/init.d/wpupdate", shell=True).wait() 60 | subprocess.Popen("update-rc.d wpupdate defaults", shell=True).wait() 61 | 62 | print "[*] Finished. If you want to update WPUpdate go to /usr/share/wpupdate and type 'git pull'" 63 | 64 | choice = raw_input("Would you like to start WPUpdate now? [y/n]: ") 65 | if choice == "yes" or choice == "y": 66 | subprocess.Popen("/etc/init.d/wpupdate start", shell=True).wait() 67 | print "[*] Installation complete. Wordpress will update every morning at 2am." 68 | 69 | if answer == "uninstall": 70 | os.remove("/etc/init.d/wpupdate") 71 | subprocess.Popen("rm -rf /usr/share/wpupdate", shell=True) 72 | subprocess.Popen("rm -rf /usr/share/wpupdate", shell=True) 73 | kill_wpupdate() 74 | print "[*] WPUpdate has been uninstalled. Manually kill the process if it is still running." 75 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustedsec/wpupdate/e8ef3cf2cf03e1798e95f6b0bdcf3d5d482db298/src/__init__.py -------------------------------------------------------------------------------- /src/startup_wpupdate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: wpupdate 5 | # Required-Start: $remote_fs $syslog 6 | # Required-Stop: $remote_fs $syslog 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 0 1 6 9 | # Short-Description: WPUpdate - Automatic updates for Wordpress. 10 | # Description: This is an automatic update script for Wordpress 11 | # 12 | ### END INIT INFO 13 | # wpupdate 14 | # description: wpupdate 15 | # processname: wpupdate 16 | 17 | DAEMON_PATH="/usr/share/wpupdate" 18 | 19 | DAEMON=/usr/share/wpupdate/wpupdate.py 20 | DAEMONOPTS="" 21 | 22 | NAME=wpupdate 23 | DESC="WPUpdate - automatically update wordpress" 24 | PIDFILE=/var/run/wpupdate.pid 25 | SCRIPTNAME=/etc/init.d/wpupdate 26 | 27 | case "$1" in 28 | start) 29 | printf "%-50s" "Starting WPUpdate..." 30 | cd $DAEMON_PATH 31 | PID=`$DAEMON $DAEMONOPTS > /dev/null 2>&1 & echo $!` 32 | #echo "Saving PID" $PID " to " $PIDFILE 33 | if [ -z $PID ]; then 34 | printf "%s\n" "Fail" 35 | else 36 | echo $PID > $PIDFILE 37 | printf "%s\n" "Ok" 38 | fi 39 | ;; 40 | status) 41 | printf "%-50s" "Checking WPUpdate..." 42 | if [ -f $PIDFILE ]; then 43 | PID=`cat $PIDFILE` 44 | if [ -z "`ps axf | grep ${PID} | grep -v grep`" ]; then 45 | printf "%s\n" "Process dead but pidfile exists" 46 | else 47 | echo "Running" 48 | fi 49 | else 50 | printf "%s\n" "Service not running" 51 | fi 52 | ;; 53 | stop) 54 | printf "%-50s" "Stopping WPUpdate..." 55 | PID=`cat $PIDFILE` 56 | cd $DAEMON_PATH 57 | if [ -f $PIDFILE ]; then 58 | kill -HUP $PID 59 | printf "%s\n" "Ok" 60 | rm -f $PIDFILE 61 | else 62 | printf "%s\n" "pidfile not found" 63 | fi 64 | ;; 65 | 66 | restart) 67 | $0 stop 68 | $0 start 69 | ;; 70 | 71 | *) 72 | echo "Usage: $0 {status|start|stop|restart}" 73 | exit 1 74 | esac 75 | -------------------------------------------------------------------------------- /src/wpcore.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # 4 | # functions for wpupdate 5 | # 6 | # 7 | import time 8 | import datetime 9 | import hashlib 10 | import subprocess 11 | import os 12 | import urllib2 13 | import re 14 | 15 | # check config 16 | def check_config(param): 17 | # grab the default path 18 | if os.path.isfile("config"): 19 | path = "config" 20 | else: 21 | path = "/usr/share/wpupdate/config" 22 | fileopen = file(path, "r") 23 | # iterate through lines in file 24 | for line in fileopen: 25 | if not line.startswith("#"): 26 | match = re.search(param, line) 27 | if match: 28 | line = line.rstrip() 29 | line = line.replace('"', "") 30 | line = line.split("=", 1) 31 | return line[1] 32 | 33 | # quick progress bar downloader 34 | def download_file(url,filename): 35 | u = urllib2.urlopen(url) 36 | f = open("/tmp/" + filename, 'wb') 37 | meta = u.info() 38 | file_size = int(meta.getheaders("Content-Length")[0]) 39 | print "Downloading: %s Bytes: %s" % (filename, file_size) 40 | 41 | file_size_dl = 0 42 | block_sz = 8192 43 | while True: 44 | buffer = u.read(block_sz) 45 | if not buffer: 46 | break 47 | file_size_dl += len(buffer) 48 | f.write(buffer) 49 | status = r"%10d [%3.2f%%]" % (file_size_dl, file_size_dl * 100. / file_size) 50 | status = status + chr(8)*(len(status)+1) 51 | print status, 52 | f.close() 53 | 54 | # current version 55 | def grab_current_version(): 56 | url = "https://api.wordpress.org/core/version-check/1.1/" 57 | u = urllib2.urlopen(url).readlines() 58 | counter = 0 59 | for line in u: 60 | line = line.rstrip() 61 | counter = counter + 1 62 | if counter == 3: 63 | return line 64 | 65 | # check for updates 66 | def update_check(): 67 | while 1: 68 | 69 | # grab the current version of wordpress 70 | current_version = grab_current_version() 71 | print "[*] The current version of Wordpress is: " + str(current_version) 72 | 73 | # check directories 74 | wp_path = check_config("BLOG_PATH=").split(",") 75 | update_counter = 0 76 | # for paths iterates through config file for multiple install 77 | for paths in wp_path: 78 | data = file("%s/wp-includes/version.php" % (paths), "r").readlines() 79 | for line in data: 80 | if "wp_version =" in line: 81 | line = line.rstrip() 82 | version = line.replace("$wp_version = '", "").replace("';", "") 83 | print "[*] Your version of wordpress is: " + str(version) + " for installation located in: " + paths 84 | 85 | # if we aren't running the latest - then download it 86 | if current_version != version: 87 | print "[*] Upgrade detected. Performing upgrade now for %s." % (paths) 88 | if update_counter == 0: 89 | print "[*] Downloading latest version of wordpress..." 90 | download_file("https://wordpress.org/latest.zip", "latest.zip") 91 | print "[*] Extracting file contents.." 92 | os.chdir("/tmp") 93 | subprocess.Popen("unzip /tmp/latest.zip", shell=True).wait() 94 | os.chdir("/usr/share/wpupdate") 95 | update_counter = 1 96 | 97 | # copy the files over 98 | subprocess.Popen("cp -rf /tmp/wordpress/* %s/" % (paths), shell=True).wait() 99 | print "[*] Fixing up permissions now..." 100 | subprocess.Popen("chown -R root:root %s/;chown -R www-data:www-data %s/wp-content/uploads/" % (paths,paths), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 101 | 102 | else: 103 | print "[*] You are up to date for Wordpress. Moving on to plugins." 104 | 105 | if os.path.isfile("/tmp/latest.zip"): 106 | subprocess.Popen("rm /tmp/latest.zip;rm -rf /tmp/wordpress", shell=True).wait() 107 | 108 | ## 109 | ## section here is dedicated to pulling latest plugins - can't do version checks, just need to update unfortuantely 110 | ## 111 | 112 | # first we need to grab the plugin details, we'll iterate through them 113 | os.chdir(paths + "/wp-content/plugins/") 114 | plugins = [name for name in os.listdir(".") if os.path.isdir(name)] 115 | # go through the plugins and pull the download links and install 116 | for plugin in plugins: 117 | # pull from api database: http://api.wordpress.org/plugins/info/1.0/$plugname.xml 118 | pull = urllib2.urlopen("http://api.wordpress.org/plugins/info/1.0/%s.xml" % (plugin)).readlines() 119 | counter = 0 120 | for line in pull: 121 | if "download_link" in line: 122 | # our URL to download 123 | download_link = line.replace("]]>", "").split("[CDATA[")[1].rstrip() 124 | print "[*] Pulling the latest version of: " + plugin + " from URL: " + download_link + " for site in: " + paths 125 | download_file(download_link, "plugin.zip") 126 | os.chdir("/tmp") 127 | subprocess.Popen("unzip plugin.zip;cp -rf %s %s/wp-content/plugins/;rm -rf plugin.zip;rm -rf %s" % (plugin, paths, plugin), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() 128 | print "[*] Plugin have been updated if needed..." 129 | counter = 1 130 | 131 | # if we couldn't find the plugin 132 | if counter == 0: 133 | print "[!] Unable to download update for plugin: " + plugin + " it may no longer be supported." 134 | 135 | # change back to our working directory 136 | os.chdir("/usr/share/wpupdate") 137 | 138 | # thats it! wait for a bit we use 61 seconds in case the minute hasn't passed and it was too fast 139 | print "[*] Everything has finished for this go-around. Waiting 61 seconds then sleeping for another day..." 140 | 141 | # sleep for at least a minute, 1 second 142 | time.sleep(61) 143 | # sleep until 2am 144 | t = datetime.datetime.today() 145 | future = datetime.datetime(t.year,t.month,t.day,2,0) 146 | if t.hour >= 2: 147 | future += datetime.timedelta(days=1) 148 | print "WPUpdate is now sleeping for another: " + str(future-t) 149 | time.sleep((future-t).seconds) 150 | -------------------------------------------------------------------------------- /wpupdate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # automatic installation of wordpress 4 | # 5 | # 6 | from src.wpcore import * 7 | import os 8 | import sys 9 | 10 | # first check if we are installed 11 | if not os.path.isfile("/etc/init.d/wpupdate"): 12 | print "[!] WPUpdate not installed. Run python setup.py to install" 13 | 14 | # kick start the updates 15 | update_check() 16 | --------------------------------------------------------------------------------