eZ Publish - Object Relation Attributes - Reckless Cleanup

This is NOT A FIX for the code, but if you are getting confusing results for queries due to relationships between object relation attributes which were deleted from content classes, you may use this query:

Backup your database first

This is not for the faint of heart

There are no warranties or other guarantees - use this at your own risk.

DELETE FROM ezcontentobject_link WHERE contentclassattribute_id != 0 AND NOT EXISTS (SELECT * FROM ezcontentclass_attribute WHERE ezcontentclass_attribute.id = ezcontentobject_link.contentclassattribute_id);

Twitter Application Auth Sample - PHP

This is a sample PHP code which can be used to get a Twitter OAuth token for use in making API calls.

It includes a trends available request that gets the list of countries for which Twitter trends are available.

Be sure to read the documentation at the link above. A given application can only have one token at any given time, so once established, the token should be stored and reused.


        $consumerKey = '-- YOUR CONSUMER KEY --';
        $consumerSecret = '-- YOUR CONSUMER SECRET --';
        $encodedKey = urlencode($consumerKey);
        $encodedSecret = urlencode($consumerSecret);
        $bearerTokenCredentials = $encodedKey.':'.$encodedSecret;
       
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, 'https://api.twitter.com/oauth2/token');
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_ENCODING, 'gzip');
        curl_setopt($ch, CURLOPT_HTTPHEADER,
                array('Content-Type: application/x-www-form-urlencoded;charset=UTF-8',
                        'Authorization: Basic '.$base64BearerTokenCredentials));
        $result = curl_exec($ch);
        $error = curl_errno($ch);
        if ($error === 0) {
                $json = json_decode($result);
                curl_setopt($ch, CURLOPT_URL, 'https://api.twitter.com/1.1/trends/available.json');
                curl_setopt($ch, CURLOPT_HTTPGET, true);
                curl_setopt($ch, CURLOPT_POST, false);
                curl_setopt($ch, CURLOPT_HTTPHEADER,
                                array('Authorization: Bearer '.$json->access_token));
                $result = curl_exec($ch);
                $error = curl_errno($ch);
                curl_close($ch);
                if ($error === 0) {
                        $json = json_decode($result);
                        $countries = array();
                        foreach ($json as $location) {
                                if ($location->placeType->name == 'Country') {
                                        $countries[$location->woeid] = $location->name;
                                }
                        }
                        asort($countries);
                }
        }

git "Permission denied (publickey,keyboard-interactive)."

If you are getting permission denied when working with git on a remote server using a key, this may help.

First test to ensure the key will be accepted by the remote server.


ssh -v git@git.example.com

Look for these lines in the output:

debug1: Next authentication method: publickey
debug1: Offering public key: /home/account/.ssh/example.key
debug1: Server accepts key: pkalg ssh-rsa blen 277
debug1: Authentication succeeded (publickey).

Then check your ~/.ssh/config file. Be sure the user is in the file and matches what worked with the ssh test.

~/.ssh/config

Host git.example.com
    User git
    IdentityFile ~/.ssh/example.key

Now get back to work.

:)

Amazon S3 Backup

This is the third tier of a backup system, the last resort if everything has been destroyed or corrupted. This script can run on a local machine or elsewhere. I chose to run it locally because the credentials are not on a publicly accessible server. The local machine copies the data from the publicly accessible servers, stores it, then sends it to S3.

The first step is to sign up at Amazon for an S3 account, create a bucket and a user. Limit the privileges for the user as much as possible, for this script, the user needs only the putObject privilege.

The script is written in Ruby. It reads JSON configuration file which contains all the servers, files and databases to be backed up.

JSON file syntax:

{
"email": "user@localhost",
"servers": {
"example.com": {
"login": "username",
"password" : "password",
"databases": [ { "name": "database_name", "dbuser": "user", "dbpass": "password"} ],
"files": ["backup.tgz"] }
},
"s3": {
"bucket": "example.com",
"username": "user",
"accesskeyid": "-- S3 Access Key Id --",
"secretaccesskey": "-- S3 Secret Access Key --"
}
}

Each server can include multiple databases and files. Be sure to limit the privileges for this database user to SELECT and LOCK TABLES, which makes them effectively read only. Be sure to grant remote access to the database for the backup server.

The files are to be placed in a directory where they can be retrieved with wget - in the example above it would be http://example.com/backup.tgz. The intent of these files is that they contain content already publicly available. This is NOT a place to put the application configuration settings.

Each server will have a hierarchy like this:

example.com
|-- initial.tgz
`-- 20140101093022
|-- backup.tgz
`-- database_name.sql.tgz

Create initial.tgz manually - run the tar command at the top of the account, download it to your local machine, then upload it to S3. If you want to get it to S3 from the server, that's fine, just be careful not to ever leave your S3 credentials on the source server.

This is the backup script. It uses wget to get the files (you can use scp, but then you may have a credential issue), and dumps the database.


#!/usr/bin/env ruby

require 'json'
require 'net/smtp'
require 'rubygems'
require 'aws-sdk'

class ItemStatus
	def initialize(item_name, exit_status, ls_file)
		@item_name, @exit_status, @ls_file = item_name, exit_status, ls_file
	end

	def name
		return @item_name
	end

	def error
		return @download_exit_status != 0 
	end
end

json = File.read('config/.json')
parms = JSON.load(json)

if parms["email"].nil? || parms["email"].empty?
	to_email = "user@localhost"
else
	to_email = parms["email"]
end

s3 = AWS::S3.new(
  :access_key_id => parms['s3']['accesskeyid'],
  :secret_access_key => parms['s3']['secretaccesskey']
)

backup_dir = "servers"
bucket = s3.buckets[parms['s3']['bucket']]

backup = Array.new
parms["servers"].each_pair {|server_name, server|
	puts "Server: #{server_name}"
	if !server.empty?
		date = `date "+%Y%m%d%H%M"|tr -d "\n"`
		dir = backup_dir + "/" + server_name + "/" + date
		mkdir = `mkdir -p "#{dir}"`
		if $?.exitstatus === 0
			dir_created = true
			if !server["files"].nil? && !server["files"].empty?
				files = server["files"]
                                if (files.length > 0)
				        if !server["login"].nil? && !server["password"].nil?
						files.each {|file_name|
							dir_file_name = "#{dir}/#{file_name}"
							Net::SSH.start("#{server_name}", "#{server["login"]}", :password => "#{server["password"]}") do |ssh|
								ssh.scp.download! "#{file_name}", "#{dir_file_name}"
							end
							`ls -l "#{dir_file_name}"`
							backup.push(ItemStatus.new("#{file_name}", $?.exitstatus, `ls -l "#{dir_file_name}"`))		
							bucket.objects[dir_file_name].write(Pathname.new(dir_file_name));
						}
					else
						files.each {|file_name|
							dir_file_name = "#{dir}/#{file_name}"
							`wget -q http://"#{server_name}"/"#{file_name}" -O "#{dir_file_name}"`
							backup.push(ItemStatus.new("#{file_name}", $?.exitstatus, `ls -l "#{dir_file_name}"`))		
							bucket.objects[dir_file_name].write(Pathname.new(dir_file_name));
						}
					end
                                end
			end
			if !server["databases"].nil? && !server["databases"].empty?
				databases = server["databases"]
				if (databases.length > 0)
					databases.each {|db|
						dbvalues = db.values_at("name", "dbuser", "dbpass").delete_if {|v| v.nil? || v.empty?}
						if dbvalues.length === 3
							dir_file_name = "#{dir}/#{db["name"]}.sql"
							dump = `mysqldump -C #{db["name"]} -u"#{db["dbuser"]}" -p"#{db["dbpass"]}" -h"#{server_name}" > "#{dir_file_name}"`
							backup.push(ItemStatus.new(db["name"], $?.exitstatus, `ls -l "#{dir_file_name}"`))
							tar_file_name = dir_file_name + ".tgz"
							tar = `tar czf #{tar_file_name} #{dir_file_name}`
							backup.push(ItemStatus.new(tar_file_name, $?.exitstatus, `ls -l "#{tar_file_name}"`))
							bucket.objects[tar_file_name].write(Pathname.new(tar_file_name));
						end
					}
				end
			end
		else
			dir_created = false
		end
	end
	error = backup.select{|item| item.error}
	if error.length == 0
		`find -mindepth 1 -mtime +8 | xargs --no-run-if-empty rm -rf`
	end
	msg = <<END_OF_MESSAGE
To: Me #{to_email}
Subject: #{server_name} backup status

END_OF_MESSAGE

	if !server.empty?
		if dir_created
			msg = msg + "Created #{dir} okay\n\n"
			if backup.length > 0
				msg = msg + "Files\n"
				backup.each {|v|
					msg = msg + "\t" + v.to_s
				}
				msg = msg + "\nColumns\n\t1. Source\n\t2. Exit Status\n\t3. File Information\n"
			end
		else
			msg = msg + "mkdir #{dir} failed"
		end
	else
		msg = msg + "No backup configuration"
	end
	msg = msg + "\n\n\n"
	begin
		Net::SMTP.start('localhost', 25) do |smtp|
			smtp.send_message msg,'amazon@localhost', to_email
		end
	rescue
		puts "Mail send failed"
	end

}

Finally, create a cron job to run the script as needed.

It is assumed that version control for the code is handled elsewhere. This backup is for data, with an emergency copy of the code. If the code is updated, it must be manually updated.

A note about leaving the password in the config file. I understand it is a security issue. That's why this is running on a local machine. Is it completely secure? No. But it isn't on a publicly accessible server either. Could I spend more time making it secure? Absolutely. Am I going to? Probably not.

eZ 4.x Custom User Group Assignment Workflow

This is a simple workflow event that will extract a member type attribute (single select) and use it to assign the member into a group on register.

It catches not only self-registration, but updates made through the admin interface.

<?php
class HPMemberRegisterType extends eZWorkflowEventType
{
	const WORKFLOW_TYPE_STRING = 'hpmemberregister';

	public function __construct() 
	{
		parent::__construct( HPMemberRegisterType::WORKFLOW_TYPE_STRING, 'HP Member Register' );
	}
	
	public function execute ( $process, $event )
	{
		$parameters = $process->attribute( 'parameter_list' );
		
		$ini = eZINI::instance( 'hpmember.ini' );

		$objectID = $parameters['object_id']; 
		$object = eZContentObject::fetch( $objectID );
		$nodeID = $object->attribute( 'main_node_id' );
		$node = eZContentObjectTreeNode::fetch( $nodeID );

		if ( $object->contentClassIdentifier() === 'member' ) {

			$dataMap = $object->dataMap();
			$memberTypeValue = $dataMap[ 'member_type' ]->content();
			$contentClass = $object->contentClass();
			$memberTypes = $contentClass->fetchAttributeByIdentifier( 'member_type' )->content();
			$memberType = $memberTypes['options'][$memberTypeValue[0]]['name'];
			$memberGroup = $ini->variable( 'MemberGroup', $memberType );
			if ( $memberGroup !== null ) 
			{
				$node->setAttribute ( 'parent_node_id', $memberGroup );
				$node->store();
			}
		}

		return eZWorkflowType::STATUS_ACCEPTED;
	}
}
eZWorkflowEventType::registerEventType( HPMemberRegisterType::WORKFLOW_TYPE_STRING, 'hpmemberregistertype' );

.ini file:

<?php /* #?ini charset="utf-8"?

# The node id members with that type should be placed in
[MemberGroup]
Contractor=60
Homeowner=61
Lender=62
Realtor=63

*/ ?>

You can place your nodes where you like. Be sure the names under [MemberGroup] are identical to the options in the selection attribute added to the class used for user registration. This code uses a custom Member class to distinguish eZ Users from site members.