compostr 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,100 @@
1
+ module Compostr
2
+ class EntityCache
3
+ attr_accessor :cpt_class, :name_id_map, :uuid_id_map
4
+ attr_accessor :full_data
5
+
6
+ # cpt_class has to be descendant of Compostr::CustomPostType
7
+ def initialize cpt_class
8
+ @cpt_class = cpt_class
9
+ @name_id_map = nil
10
+ @uuid_id_map = nil
11
+ if !(@cpt_class < Compostr::CustomPostType)
12
+ raise "Unsupported Entity for EntityCache: #{@cpt_class}"
13
+ end
14
+ end
15
+
16
+ # Pretty nasty stuff. Stores all posts of cpt in memory (alas,
17
+ # only once) and looks through them until one with uuid found.
18
+ def in_mem_lookup uuid
19
+ # TODO index (hash) on (uu)id, access index only
20
+ if full_data.length == 10_000
21
+ # warn heavily
22
+ elsif full_data.length > 1000
23
+ # warn softly
24
+ end
25
+ uuid_selector = lambda do |x|
26
+ x["custom_fields"].find do |f|
27
+ f["key"] == "uuid" && f["value"] == uuid
28
+ end
29
+ end
30
+ full_data.find &uuid_selector
31
+ #@full_data.find &WPEvent::Lambdas.with_cf_uuid(uuid)
32
+ end
33
+
34
+ def id_of_name name
35
+ return [] if name.nil? || name.empty?
36
+ name_id_map[name]
37
+ end
38
+
39
+ def id_of_names names
40
+ return [] if names.nil? || names.empty?
41
+ names.map{|name| name_id_map[name]}
42
+ end
43
+
44
+ def id_of_uuid uuid
45
+ return [] if uuid.nil? || uuid.empty?
46
+ uuid_id_map[uuid]
47
+ end
48
+
49
+ def id_of_uuids uuids
50
+ return [] if uuids.nil? || uuids.empty?
51
+ uuids.map{|uuid| id_of_uuid uuid}
52
+ end
53
+
54
+ # init and return @name_id_map
55
+ def name_id_map
56
+ @name_id_map ||= full_data.map {|p| [p["post_title"], p["post_id"]]}.to_h
57
+ end
58
+
59
+ def uuid_id_map
60
+ @uuid_id_map ||= uuid_pid_map
61
+ end
62
+
63
+ # Burn-in cache
64
+ def full_data
65
+ @full_data ||= get_all_posts
66
+ end
67
+
68
+ # Select entities for which given selector returns true
69
+ # example: selector = lambda {|e| e.to_s == 'keepme'}
70
+ def select_by selector
71
+ full_data.select &selector
72
+ end
73
+
74
+ def by_post_id id
75
+ if @full_data.nil?
76
+ Compostr::wp.getPost blog_id: 0,
77
+ post_id: id
78
+ else
79
+ @full_data.find do |p|
80
+ p["post_id"] == id
81
+ end
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def get_all_posts
88
+ Compostr::wp.getPosts blog_id: 0,
89
+ filter: { post_type: @cpt_class.post_type, number: 100_000 }
90
+ end
91
+
92
+ def uuid_pid_map
93
+ full_data.map do |post|
94
+ # TODO lambda
95
+ uuid = post["custom_fields"].find {|f| f["key"] == "uuid"}&.fetch("value", nil)
96
+ [uuid, post["post_id"]]
97
+ end.to_h
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,31 @@
1
+ require 'mime-types'
2
+
3
+ module Compostr
4
+ class ImageUpload
5
+ attr_accessor :file_path, :post_id
6
+
7
+ def initialize file_path, post_id=nil
8
+ # TODO decide on getMediaLibrary to find an already uploaded image
9
+ @file_path = file_path
10
+ @post_id = post_id
11
+ end
12
+
13
+ # Push data to Wordpress instance, return attachment_id
14
+ def do_upload!
15
+ data = create_data
16
+ response = Compostr::wp.uploadFile(data: data)
17
+ response["attachment_id"]
18
+ end
19
+
20
+ private
21
+
22
+ def create_data
23
+ {
24
+ name: File.basename(@file_path),
25
+ type: MIME::Types.type_for(file_path).first.to_s,
26
+ post_id: @post_id || '',
27
+ bits: XMLRPC::Base64.new(IO.read file_path)
28
+ }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ require 'mime-types'
2
+
3
+ module Compostr
4
+ class ImageUploader
5
+ attr_accessor :image_store
6
+ attr_accessor :media_cache
7
+
8
+ def initialize image_store, media_cache
9
+ if !image_store
10
+ Compostr.logger.warn "ImageUploader will not uploading anything (no image store specified)."
11
+ end
12
+ @image_store = image_store
13
+ @media_cache = media_cache
14
+ end
15
+
16
+ # Returns attachment id of an image already uploaded
17
+ # or attachment_id of image after fresh upload
18
+ # (or nil if path empty)
19
+ def process rel_path, wp_event=nil
20
+ return nil if rel_path.to_s.strip.empty?
21
+ return nil if @image_store.nil?
22
+
23
+ # Do we need URI encoding here?
24
+ if attachment_id = @media_cache.id_of_name(rel_path)
25
+ Compostr.logger.debug "Image already uploaded."
26
+ else
27
+ path = File.join(@image_store, rel_path)
28
+ Compostr.logger.info "Uploading file #{path}"
29
+ upload = Compostr::ImageUpload.new(path)
30
+ attachment_id = upload.do_upload!
31
+ Compostr.logger.debug "Uploaded image id: #{attachment_id}"
32
+ end
33
+
34
+ return attachment_id
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ require 'logger'
2
+
3
+ module Compostr
4
+ # Module to extend to get easy access to standard log functions.
5
+ # These are debug, info, warn, error and fatal.
6
+ # All log functions use the Compostr.logger (which can be customized).
7
+ # A typical client will just `extend Compostr::Logging` .
8
+ module Logging
9
+ def debug msg
10
+ Compostr.logger.debug msg
11
+ end
12
+ def info msg
13
+ Compostr.logger.info msg
14
+ end
15
+ def warn msg
16
+ Compostr.logger.warn msg
17
+ end
18
+ def error msg
19
+ Compostr.logger.error msg
20
+ end
21
+ def fatal msg
22
+ Compostr.logger.fatal msg
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,51 @@
1
+ module Compostr
2
+ # Cache name->id for media items
3
+ # pretty wet copy of entitycache. Unclear how to DRY this up.
4
+ # Wordpress apparently lowercases file endings on upload, so this fact
5
+ # is respected in the lookup (only file ending is modified).
6
+ class MediaLibraryCache
7
+ attr_accessor :name_id_map
8
+
9
+ def initialize
10
+ @name_id_map = nil
11
+ end
12
+
13
+ # return id of given name, initializing the cache
14
+ # if necessary
15
+ def id_of_name name
16
+ return [] if name.nil? || name.empty?
17
+ # Downcase file ending
18
+ name_for_wp = File.basename(name, '.*') + File.extname(name).downcase
19
+ name_id_map[name_for_wp]
20
+ end
21
+
22
+ # return array of ids to given names, initializing the cache
23
+ # if necessary
24
+ def id_of_names names
25
+ return [] if names.nil? || names.empty?
26
+ names.map{|name| id_of_name name }
27
+ end
28
+
29
+ # init and return @name_id_map
30
+ def name_id_map
31
+ if @name_id_map.nil?
32
+ @name_id_map = create_name_id_map
33
+ end
34
+ @name_id_map || {}
35
+ end
36
+
37
+ private
38
+ def create_name_id_map
39
+ items = Compostr::wp.getMediaLibrary(blog_id: 0)
40
+
41
+ # warn if size is interesting.
42
+
43
+ items.map do |i|
44
+ uri = URI.parse URI.encode(i['link'])
45
+ filename = File.basename uri.path
46
+
47
+ [filename, i['attachment_id']]
48
+ end.to_h
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,88 @@
1
+ module Compostr
2
+ # Magically syncs custom posts between json, intermediate representation
3
+ # or other forms with data in wordpress. Scary API award.
4
+ class Syncer
5
+ include Compostr::Logging
6
+
7
+ attr_accessor :image_uploader
8
+ attr_accessor :synced_uuids
9
+ attr_accessor :updated_uuids
10
+
11
+ def initialize image_uploader
12
+ @image_uploader = image_uploader
13
+ @synced_uuids = []
14
+ @updated_uuids = []
15
+ end
16
+
17
+ # Updates or creates Custom Post Types Posts.
18
+ #
19
+ # The post will be identified by uuid (or not).
20
+ #
21
+ # new_post_content
22
+ # The data that **should** be in wordpress (without knowing
23
+ # of wordpress post or custom field ids). Usually descendant of CustomPostType.
24
+ #
25
+ # old_post
26
+ # The data currently available in wordpress (including
27
+ # wordpress post id, custom field ids).
28
+ def merge_push new_post, old_post
29
+ if old_post && old_post.in_wordpress?
30
+ info "#{new_post.class.name} with UUID #{new_post.uuid} found, updating"
31
+ debug old_post.fields.inspect
32
+
33
+ new_post.different_from? old_post
34
+
35
+ new_post.post_id = old_post.post_id
36
+ new_post.integrate_field_ids old_post
37
+
38
+ # TODO unclear how to deal with images
39
+ #attachment_id = @image_uploader.process json['image_url']
40
+ #new_entity.featured_image_id = attachment_id
41
+
42
+ #TODO one (cat) is missing when updating single via bundle json
43
+
44
+ content = new_post.to_content_hash
45
+ adjust_content content
46
+ # TODO and image ...
47
+
48
+ debug "Upload Post ##{new_post.post_id} with wp-content: #{content}"
49
+
50
+ post_id = Compostr::wp.editPost(blog_id: 0,
51
+ post_id: new_post.post_id,
52
+ content: content)
53
+ if post_id
54
+ info "#{new_post.class} ##{new_post.post_id} updated"
55
+ else
56
+ info "#{new_post.class} ##{new_post.post_id} not updated!"
57
+ end
58
+ else
59
+ # Easy, create new one
60
+ info "#{new_post.class.name} with UUID #{new_post.uuid} not found, creating"
61
+ content = new_post.to_content_hash
62
+ adjust_content content
63
+
64
+ # Ouch ....
65
+ #attachment_id = @image_uploader.process json['image_url']
66
+ #custom_post.featured_image_id = attachment_id
67
+
68
+ debug "Create Post with wp-content: #{content}"
69
+
70
+ new_post_id = Compostr::wp.newPost(blog_id: 0,
71
+ content: content)
72
+ if new_post_id
73
+ info "#{new_post.class} with WP ID #{new_post_id} created"
74
+ else
75
+ info "#{new_post.class} not created!"
76
+ end
77
+ new_post.post_id = new_post_id
78
+ end
79
+ end
80
+
81
+ # Add language term and post author data to WP content hash.
82
+ def adjust_content content
83
+ content[:terms_names] = { 'language' => [ Compostr::config.language_term || 'Deutsch' ] }
84
+ content[:post_author] = Compostr::config.author_id || 1
85
+ # publish? Date ... ?
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,3 @@
1
+ module Compostr
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,14 @@
1
+ module Compostr
2
+ module WPString
3
+ def self.wp_string string
4
+ content = string.to_s.strip
5
+ if !content.match(/img/)
6
+ re = /<("[^"]*"|'[^']*'|[^'">])*>/
7
+ if content.gsub(re, '') == ''
8
+ return ''
9
+ end
10
+ end
11
+ content
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,62 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: post
5
+ uri: http://wordpress.mydomain/xmlrpc.php
6
+ body:
7
+ encoding: UTF-8
8
+ string: '<?xml version="1.0" ?><methodCall><methodName>wp.newPost</methodName><params><param><value><array><data><value><i4>0</i4></value><value><string>admin</string></value><value><string>buzzword</string></value><value><struct><member><name>post_type</name><value><string>boardgame</string></value></member><member><name>post_status</name><value><string>publish</string></value></member><member><name>post_data</name><value><dateTime.iso8601>20170518T11:42:23</dateTime.iso8601></value></member><member><name>post_title</name><value><string>Dungeonlord</string></value></member><member><name>post_content</name><value><string></string></value></member><member><name>custom_fields</name><value><array><data/></array></value></member><member><name>terms_names</name><value><struct><member><name>language</name><value><array><data><value><string>Deutsch</string></value></data></array></value></member></struct></value></member><member><name>post_author</name><value><i4>1</i4></value></member></struct></value></data></array></value></param></params></methodCall>
9
+
10
+ '
11
+ headers:
12
+ User-Agent:
13
+ - XMLRPC::Client (Ruby 2.3.1)
14
+ Content-Type:
15
+ - text/xml; charset=utf-8
16
+ Content-Length:
17
+ - '1056'
18
+ Connection:
19
+ - keep-alive
20
+ Accept-Encoding:
21
+ - identity
22
+ Accept:
23
+ - "*/*"
24
+ response:
25
+ status:
26
+ code: 200
27
+ message: OK
28
+ headers:
29
+ Date:
30
+ - Thu, 18 May 2017 09:31:43 GMT
31
+ Server:
32
+ - Apache/2.2.14 (Ubuntu)
33
+ X-Powered-By:
34
+ - PHP/5.3.2-1ubuntu4.30
35
+ Connection:
36
+ - close
37
+ X-Content-Security-Policy:
38
+ - allow *
39
+ Content-Security-Policy:
40
+ - frame-ancestors 'self'
41
+ Vary:
42
+ - Accept-Encoding
43
+ Content-Length:
44
+ - '179'
45
+ Content-Type:
46
+ - text/xml; charset=UTF-8
47
+ body:
48
+ encoding: UTF-8
49
+ string: |
50
+ <?xml version="1.0" encoding="UTF-8"?>
51
+ <methodResponse>
52
+ <params>
53
+ <param>
54
+ <value>
55
+ <string>2766</string>
56
+ </value>
57
+ </param>
58
+ </params>
59
+ </methodResponse>
60
+ http_version:
61
+ recorded_at: Thu, 18 May 2017 09:42:25 GMT
62
+ recorded_with: VCR 3.0.3
@@ -0,0 +1,61 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: post
5
+ uri: http://wordpress.mydomain/xmlrpc.php
6
+ body:
7
+ encoding: UTF-8
8
+ string: '<?xml version="1.0" ?><methodCall><methodName>wp.deletePost</methodName><params><param><value><array><data><value><i4>0</i4></value><value><string>admin</string></value><value><string>buzzword</string></value><value><string>100000</string></value></data></array></value></param></params></methodCall>
9
+
10
+ '
11
+ headers:
12
+ User-Agent:
13
+ - XMLRPC::Client (Ruby 2.3.1)
14
+ Content-Type:
15
+ - text/xml; charset=utf-8
16
+ Content-Length:
17
+ - '298'
18
+ Connection:
19
+ - keep-alive
20
+ Accept-Encoding:
21
+ - identity
22
+ Accept:
23
+ - "*/*"
24
+ response:
25
+ status:
26
+ code: 200
27
+ message: OK
28
+ headers:
29
+ Date:
30
+ - Mon, 22 May 2017 14:04:20 GMT
31
+ Server:
32
+ - Apache/2.2.14 (Ubuntu)
33
+ X-Powered-By:
34
+ - PHP/5.3.2-1ubuntu4.30
35
+ Connection:
36
+ - close
37
+ X-Content-Security-Policy:
38
+ - allow *
39
+ Content-Security-Policy:
40
+ - frame-ancestors 'self'
41
+ Vary:
42
+ - Accept-Encoding
43
+ Content-Length:
44
+ - '395'
45
+ Content-Type:
46
+ - text/xml; charset=UTF-8
47
+ body:
48
+ encoding: ASCII-8BIT
49
+ string: !binary |-
50
+ PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPG1ldGhv
51
+ ZFJlc3BvbnNlPgogIDxmYXVsdD4KICAgIDx2YWx1ZT4KICAgICAgPHN0cnVj
52
+ dD4KICAgICAgICA8bWVtYmVyPgogICAgICAgICAgPG5hbWU+ZmF1bHRDb2Rl
53
+ PC9uYW1lPgogICAgICAgICAgPHZhbHVlPjxpbnQ+NDA0PC9pbnQ+PC92YWx1
54
+ ZT4KICAgICAgICA8L21lbWJlcj4KICAgICAgICA8bWVtYmVyPgogICAgICAg
55
+ ICAgPG5hbWU+ZmF1bHRTdHJpbmc8L25hbWU+CiAgICAgICAgICA8dmFsdWU+
56
+ PHN0cmluZz5VbmfDvGx0aWdlIEJlaXRyYWdzLUlELjwvc3RyaW5nPjwvdmFs
57
+ dWU+CiAgICAgICAgPC9tZW1iZXI+CiAgICAgIDwvc3RydWN0PgogICAgPC92
58
+ YWx1ZT4KICA8L2ZhdWx0Pgo8L21ldGhvZFJlc3BvbnNlPgo=
59
+ http_version:
60
+ recorded_at: Mon, 22 May 2017 14:15:31 GMT
61
+ recorded_with: VCR 3.0.3